From 644e5c5d3d08e1b4fc74d84046128b4eabd4d95c Mon Sep 17 00:00:00 2001 From: "Maxime Alves LIRMM@home" Date: Thu, 9 Jul 2020 08:02:44 +0200 Subject: [PATCH] [conf] add envfile supprt / fix the bugs related to acl_caller_middleware / fix the bugs related to responses classes --- conf/env.example | 5 +- halfapi/app.py | 14 ++--- halfapi/cli.py | 14 ++++- halfapi/lib/acl_caller_middleware.py | 30 +++++++++-- halfapi/lib/responses.py | 11 ++-- poetry.lock | 79 +++++++++++++++++++++++++--- pyproject.toml | 4 +- 7 files changed, 129 insertions(+), 28 deletions(-) diff --git a/conf/env.example b/conf/env.example index d6d522a..a119d91 100644 --- a/conf/env.example +++ b/conf/env.example @@ -1,2 +1,3 @@ -export HALFORM_SECRET="halform_secret" -export HALFORM_DSN="dbname=api user=api password= host=127.0.0.1 port=5432" +HALFORM_SECRET="halform_secret" +HALFORM_DSN="dbname=api user=api password= host=127.0.0.1 port=5432" +DEBUG=1 diff --git a/halfapi/app.py b/halfapi/app.py index 3b22528..0606adb 100644 --- a/halfapi/app.py +++ b/halfapi/app.py @@ -100,16 +100,18 @@ def check_conf(): if not environ.get('HALFORM_DSN', False): print('Missing HALFORM_DSN variable from configuration') -DEBUG = True +CONFIG={ + 'DEBUG' : 'DEBUG' in environ.keys() +} debug_routes = [ - Route('/', lambda request: PlainTextResponse('It Works!')), - Route('/user', lambda request: JSONResponse({'user':request.user})), - Route('/payload', lambda request: JSONResponse({'payload':request.payload})) -] if DEBUG is True else [] + Route('/', lambda request, *args, **kwargs: PlainTextResponse('It Works!')), + Route('/user', lambda request, *args, **kwargs: JSONResponse({'user':str(request.user)})), + Route('/payload', lambda request, *args, **kwargs: JSONResponse({'payload':str(request.payload)})) +] if CONFIG['DEBUG'] is True else [] app = Starlette( - debug=DEBUG, + debug=CONFIG['DEBUG'], routes=debug_routes, middleware=[ Middleware(AuthenticationMiddleware, backend=JWTAuthenticationBackend(secret_key=environ.get('HALFORM_SECRET'))), diff --git a/halfapi/cli.py b/halfapi/cli.py index df0e0d1..51b7612 100755 --- a/halfapi/cli.py +++ b/halfapi/cli.py @@ -32,6 +32,7 @@ def cli(ctx): if ctx.invoked_subcommand is None: return run() +@click.option('--envfile', default=None) @click.option('--host', default='127.0.0.1') @click.option('--port', default='8000') @click.option('--debug', default=False) @@ -42,7 +43,17 @@ def cli(ctx): @click.option('--dbuser', default='api') @click.option('--dbpassword', default='') @cli.command() -def run(host, port, debug, dev, dbname, dbhost, dbport, dbuser, dbpassword): +def run(envfile, host, port, debug, dev, dbname, dbhost, dbport, dbuser, dbpassword): + if envfile: + try: + with open(envfile) as f: + print('Will use the following env parameters') + print(f.readlines()) + pass + except FileNotFoundError: + print(f'No file named {envfile}') + envfile = None + if dev: debug = True reload = True @@ -87,6 +98,7 @@ def run(host, port, debug, dev, dbname, dbhost, dbport, dbuser, dbpassword): sys.path.insert(0, os.getcwd()) click.echo(sys.path) uvicorn.run('halfapi.app:app', + env_file=envfile, host=host, port=int(port), log_level=log_level, diff --git a/halfapi/lib/acl_caller_middleware.py b/halfapi/lib/acl_caller_middleware.py index f9c14e5..8912732 100644 --- a/halfapi/lib/acl_caller_middleware.py +++ b/halfapi/lib/acl_caller_middleware.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from os import environ + from starlette.exceptions import HTTPException from starlette.middleware.base import BaseHTTPMiddleware from starlette.routing import Match, Mount @@ -6,7 +8,11 @@ from starlette.types import ASGIApp, Receive, Scope, Send from halfapi.models.api.view.acl import Acl as AclView -def match_route(scope: Scope): +class DebugRouteException(Exception): + def __init__(self, *args, **kwargs): + super().__init__(self) + +def match_route(app: ASGIApp, scope: Scope): """ Checks all routes from "app" and checks if it matches with the one from scope @@ -30,7 +36,10 @@ def match_route(scope: Scope): Refer to the database documentation for more details on the api.route table. """ - from halfapi.app import app + + print('3') + from ..app import CONFIG + print(CONFIG) result = { 'domain': None, @@ -39,6 +48,9 @@ def match_route(scope: Scope): 'version': None } + if 'DEBUG' in CONFIG.keys() and len(scope['path'].split('/')) <= 3: + raise DebugRouteException() + try: """ Identification of the parts of the path @@ -51,7 +63,6 @@ def match_route(scope: Scope): except ValueError as e: #404 Not found raise HTTPException(404) - # Prefix the path with "/" path = f'/{path}' @@ -111,13 +122,18 @@ class AclCallerMiddleware(BaseHTTPMiddleware): await self.app(scope, receive, send) return + app = self.app + while True: + if not hasattr(app, 'app'): + break + app = app.app if scope['path'].split('/')[-1] not in ['docs','openapi.json','redoc']: # routes in the the database, the others being # docs/openapi.json/redoc - d_match, path_params = match_route(scope) try: + d_match, path_params = match_route(app, scope) scope['acls'] = [] for acl in AclView(**d_match).select(): # retrieve related ACLs @@ -132,5 +148,11 @@ class AclCallerMiddleware(BaseHTTPMiddleware): # TODO : No ACL sur une route existante, prevenir l'admin? print("No ACL") pass + except DebugRouteException: + print("Debug route") + if 'DEBUG_ACL' in environ.keys(): + scope['acls'] = environ['DEBUG_ACL'].split(':') + else: + scope['acls'] = [] return await self.app(scope, receive, send) diff --git a/halfapi/lib/responses.py b/halfapi/lib/responses.py index d4a5c3c..679d446 100644 --- a/halfapi/lib/responses.py +++ b/halfapi/lib/responses.py @@ -5,12 +5,13 @@ from datetime import date from io import TextIOBase, StringIO # asgi framework -from starlette.responses import Response +from starlette.responses import PlainTextResponse, Response __all__ = ['CSVResponse', 'InternalServerErrorResponse', 'NotFoundResponse', 'NotImplementedResponse', + 'PlainTextResponse', 'UnauthorizedResponse'] class CSVResponse(Response): @@ -32,27 +33,27 @@ class CSVResponse(Response): class InternalServerErrorResponse(Response): """ The 500 Internal Server Error default Response """ - def __init__(self): + def __init__(self, *args, **kwargs): super().__init__(status_code=500) class NotFoundResponse(Response): """ The 404 Not Found default Response """ - def __init__(self): + def __init__(self, *args, **kwargs): super().__init__(status_code=404) class NotImplementedResponse(Response): """ The 501 Not Implemented default Response """ - def __init__(self): + def __init__(self, *args, **kwargs): super().__init__(status_code=501) class UnauthorizedResponse(Response): """ The 401 Not Found default Response """ - def __init__(self): + def __init__(self, *args, **kwargs): super().__init__(status_code = 401) diff --git a/poetry.lock b/poetry.lock index d8d4beb..5976f7f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,7 @@ [[package]] category = "dev" description = "Atomic file writes." +marker = "sys_platform == \"win32\"" name = "atomicwrites" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -53,6 +54,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "0.4.3" +[[package]] +category = "main" +description = "Handle .env files" +name = "dotenv" +optional = false +python-versions = "*" +version = "0.0.5" + [[package]] category = "main" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" @@ -95,6 +104,7 @@ psycopg2-binary = "*" reference = "fe53195abb637d2192857e8b4878f4865b0fcce4" type = "git" url = "git@gite.lirmm.fr:newsi/halfORM.git" + [[package]] category = "main" description = "A collection of framework independent HTTP protocol utils." @@ -159,6 +169,19 @@ uvicorn = "*" reference = "4f9cf92253b2ee526a528724877840c8ec158d3a" type = "git" url = "git@gite.lirmm.fr:newsi/api/organigramme.git" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.4" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + [[package]] category = "dev" description = "plugin and hook calling mechanisms for python" @@ -217,23 +240,39 @@ crypto = ["cryptography (>=1.4)"] flake8 = ["flake8", "flake8-import-order", "pep8-naming"] test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"] +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + [[package]] category = "dev" description = "pytest: simple powerful testing with Python" name = "pytest" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.10.1" +python-versions = ">=3.5" +version = "5.4.3" [package.dependencies] atomicwrites = ">=1.0" attrs = ">=17.4.0" colorama = "*" more-itertools = ">=4.0.0" -pluggy = ">=0.7" +packaging = "*" +pluggy = ">=0.12,<1.0" py = ">=1.5.0" -setuptools = "*" -six = ">=1.10.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +checkqa-mypy = ["mypy (v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] category = "main" @@ -276,6 +315,7 @@ half_orm = "*" reference = "d0f14a9631eecd29098d13a3f34e9cd533145f24" type = "git" url = "git@gite.lirmm.fr:newsi/sidb_halfORM.git" + [[package]] category = "dev" description = "Python 2 and 3 compatibility utilities" @@ -335,6 +375,14 @@ optional = false python-versions = "*" version = "0.14.0" +[[package]] +category = "dev" +description = "Measures the displayed width of unicode strings in a terminal" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.2.5" + [[package]] category = "main" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" @@ -360,7 +408,7 @@ testing = ["jaraco.itertools", "func-timeout"] organigramme = ["fastapi", "organigramme"] [metadata] -content-hash = "54154bcf65bf5540092d6493dd5810fb541bc4a75e9fc328ca66d25ef5ed0584" +content-hash = "56e1f269ca07039fa6898fa427261f61cab9ef871c5a82edb592cbfb197c2d03" python-versions = "^3.7" [metadata.files] @@ -388,6 +436,9 @@ colorama = [ {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] +dotenv = [ + {file = "dotenv-0.0.5.tar.gz", hash = "sha256:b58d2ab3f83dbd4f8a362b21158a606bee87317a9444485566b3c8f0af847091"}, +] fastapi = [ {file = "fastapi-0.58.1-py3-none-any.whl", hash = "sha256:d7499761d5ca901cdf5b6b73018d14729593f8ab1ea22d241f82fa574fc406ad"}, {file = "fastapi-0.58.1.tar.gz", hash = "sha256:92e59b77eef7d6eaa80b16d275adda06b5f33b12d777e3fc5521b2f7f4718e13"}, @@ -424,6 +475,10 @@ more-itertools = [ {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, ] organigramme = [] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, @@ -487,9 +542,13 @@ pyjwt = [ {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, {file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"}, ] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] pytest = [ - {file = "pytest-3.10.1-py2.py3-none-any.whl", hash = "sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec"}, - {file = "pytest-3.10.1.tar.gz", hash = "sha256:e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"}, + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, ] pyyaml = [ {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, @@ -536,6 +595,10 @@ uvloop = [ {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, ] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] websockets = [ {file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"}, {file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"}, diff --git a/pyproject.toml b/pyproject.toml index bd164d2..31a23c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,9 +22,9 @@ requests = "^2" [tool.poetry.extras] organigramme = [ "fastapi", "organigramme" ] +[tool.poetry.scripts] +halfapi = 'halfapi.cli:cli' [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" -[tool.poetry.scripts] -halfapi = 'halfapi.cli:cli'