From d78d581c656479b757c4868342e3d486af223205 Mon Sep 17 00:00:00 2001 From: Matthieu Lamalle Date: Fri, 24 Jan 2020 13:43:14 +0100 Subject: [PATCH] implement basicauth and jwt token --- README.md | 3 +- docker/Dockerfile | 2 +- docker/docker-compose.yaml | 1 + docker/postgres-init/10-postgres.init.sh | 2 + messages/v1/messages/user.create.yml | 4 ++ script/database_manager.py | 1 + src/risotto/config.py | 19 ++++-- src/risotto/http.py | 87 ++++++++++++++++++++++-- src/risotto/services/user/user.py | 12 +++- 9 files changed, 116 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0676113..aabb7b1 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ docker exec -ti postgres bash psql -U postgres -h localhost -c "CREATE ROLE risotto WITH LOGIN PASSWORD 'risotto';" psql -U postgres -h localhost -c "CREATE DATABASE risotto;" psql -U postgres -h localhost -c "GRANT ALL ON DATABASE risotto TO risotto;" -psql -U postgres -h localhost -c "CREATE EXTENSION hstore;" risotto +psql -U postgres -h localhost -c "CREATE EXTENSION hstore;" +psql -U postgres -h localhost -c "CREATE EXTENSION pgcrypto;" ``` Gestion de la base de données avec Sqitch diff --git a/docker/Dockerfile b/docker/Dockerfile index 03f26bf..ca12d2b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,7 +20,7 @@ RUN ln -s /srv/src/tiramisu/tiramisu /usr/local/lib/python3.7 RUN ln -s /srv/src/rougail/src/rougail /usr/local/lib/python3.7 RUN ln -s /srv/src/risotto/src/risotto /usr/local/lib/python3.7 -RUN pip install Cheetah3 +RUN pip install Cheetah3 PyJWT RUN cd /srv/src/risotto && pip install -r requirements.txt # Installation diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index dce1a1f..75c102d 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -6,6 +6,7 @@ services: dockerfile: docker/Dockerfile volumes: - ../.:/srv/src/risotto + - ../messages:/usr/local/lib/messages ports: - "8080:8080" depends_on: diff --git a/docker/postgres-init/10-postgres.init.sh b/docker/postgres-init/10-postgres.init.sh index 3cdc3bb..4baea20 100755 --- a/docker/postgres-init/10-postgres.init.sh +++ b/docker/postgres-init/10-postgres.init.sh @@ -7,6 +7,7 @@ psql --username "$POSTGRES_USER" <<-EOSQL GRANT ALL ON DATABASE risotto TO risotto; \c risotto CREATE EXTENSION hstore; + CREATE EXTENSION pgcrypto; EOSQL psql --username "risotto" --password "risotto" <<-EOSQL @@ -64,6 +65,7 @@ psql --username "risotto" --password "risotto" <<-EOSQL CREATE TABLE RisottoUser ( UserId SERIAL PRIMARY KEY, UserLogin VARCHAR(100) NOT NULL UNIQUE, + UserPassword TEXT NOT NULL, UserName VARCHAR(100) NOT NULL, UserSurname VARCHAR(100) NOT NULL ); diff --git a/messages/v1/messages/user.create.yml b/messages/v1/messages/user.create.yml index dfa28c2..13b9d80 100644 --- a/messages/v1/messages/user.create.yml +++ b/messages/v1/messages/user.create.yml @@ -11,6 +11,10 @@ parameters: shortarg: l description: Login de l'utilisateur. ref: User.Login + user_password: + type: String + shortarg: p + description: Password de l'utilisateur. user_name: type: String shortarg: n diff --git a/script/database_manager.py b/script/database_manager.py index 56017ce..65bd7c0 100644 --- a/script/database_manager.py +++ b/script/database_manager.py @@ -57,6 +57,7 @@ CREATE TABLE Server ( CREATE TABLE RisottoUser ( UserId SERIAL PRIMARY KEY, UserLogin VARCHAR(100) NOT NULL UNIQUE, + UserPassword TEXT NOT NULL, UserName VARCHAR(100) NOT NULL, UserSurname VARCHAR(100) NOT NULL ); diff --git a/src/risotto/config.py b/src/risotto/config.py index bcff151..4a7aefe 100644 --- a/src/risotto/config.py +++ b/src/risotto/config.py @@ -5,7 +5,12 @@ CONFIGURATION_DIR = 'configurations' TEMPLATE_DIR = 'templates' TMP_DIR = 'tmp' ROUGAIL_DTD_PATH = '../rougail/data/creole.dtd' -DEFAULT_USER = 'Anonymous' +DEFAULT_USER = 'gnunux' +DEFAULT_USER_PASSWORD = 'gnunux' +URI = 'http://localhost' +PORT = 8080 +JWT_SECRET = 'MY_SUPER_SECRET' +JWT_TOKEN_EXPIRE = 3600 import os from pathlib import PurePosixPath @@ -19,16 +24,20 @@ def get_config(): 'user': 'risotto', 'password': 'risotto', }, - 'http_server': {'port': 8080, - #'default_user': "gnunux"}, + 'http_server': {'port': PORT, 'default_user': DEFAULT_USER}, 'global': {'message_root_path': CURRENT_PATH.parents[2] / 'messages', 'debug': True, 'internal_user': 'internal', 'check_role': True, 'rougail_dtd_path': '../rougail/data/creole.dtd', - 'admin_user': DEFAULT_USER}, + 'admin_user': DEFAULT_USER, + 'admin_user_password': DEFAULT_USER_PASSWORD}, 'source': {'root_path': '/srv/seed'}, - 'cache': {'root_path': '/var/cache/risotto'} + 'cache': {'root_path': '/var/cache/risotto'}, + 'jwt': { + 'secret': JWT_SECRET, + 'token_expire': JWT_TOKEN_EXPIRE, + 'issuer': URI} } diff --git a/src/risotto/http.py b/src/risotto/http.py index dbef434..5887b5b 100644 --- a/src/risotto/http.py +++ b/src/risotto/http.py @@ -1,8 +1,10 @@ -from aiohttp.web import Application, Response, get, post, HTTPBadRequest, HTTPInternalServerError, HTTPNotFound +from aiohttp.web import Application, Response, get, post, HTTPBadRequest, HTTPInternalServerError, HTTPNotFound, HTTPUnauthorized +from aiohttp import BasicAuth, RequestInfo from json import dumps from traceback import print_exc from tiramisu import Config - +import datetime +import jwt from .dispatcher import dispatcher from .utils import _ @@ -16,7 +18,17 @@ from .services import load_services def create_context(request): risotto_context = Context() - risotto_context.username = request.match_info.get('username', + if 'Authorization' in request.headers: + token = request.headers['Authorization'] + if not token.startswith("Bearer "): + raise HTTPBadRequest(reason='Unexpected bearer format') + token = token[7:] + decoded = verify_token(token) + if 'user' in decoded: + risotto_context.username = decoded['user'] + return risotto_context + else: + risotto_context.username = request.match_info.get('username', get_config()['http_server']['default_user']) return risotto_context @@ -49,7 +61,7 @@ class extra_route_handler: try: returns = await cls.function(**kwargs) except NotAllowedError as err: - raise HTTPNotFound(reason=str(err)) + raise HTTPUnauthorized(reason=str(err)) except CallError as err: raise HTTPBadRequest(reason=str(err)) except Exception as err: @@ -77,7 +89,7 @@ async def handle(request): check_role=True, **kwargs) except NotAllowedError as err: - raise HTTPNotFound(reason=str(err)) + raise HTTPUnauthorized(reason=str(err)) except CallError as err: raise HTTPBadRequest(reason=str(err).replace('\n', ' ')) except Exception as err: @@ -141,8 +153,73 @@ async def get_app(loop): print() del extra_routes app.add_routes(routes) + app.router.add_post('/auth', auth) + app.router.add_post('/access_token', access_token) await dispatcher.on_join() return await loop.create_server(app.make_handler(), '*', get_config()['http_server']['port']) +async def auth(request): + auth_code = request.headers['Authorization'] + if not auth_code.startswith("Basic "): + raise HTTPBadRequest(reason='Unexpected bearer format') + auth = BasicAuth.decode(auth_code) + async with dispatcher.pool.acquire() as connection: + async with connection.transaction(): + # Check role with ACL + sql = ''' + SELECT UserName + FROM RisottoUser + WHERE UserLogin = $1 + AND UserPassword = crypt($2, UserPassword); + ''' + res = await connection.fetch(sql, auth.login, auth.password) + if res: + res = gen_token(auth) + if verify_token(res): + return Response(text=str(res.decode('utf-8'))) + else: + return HTTPInternalServerError(reason='Token could not be verified just after creation') + else: + raise HTTPUnauthorized(reason='Unauthorized') + +def gen_token(auth): + secret = get_config()['jwt']['secret'] + expire = get_config()['jwt']['token_expire'] + issuer = get_config()['jwt']['issuer'] + payload = { + 'user': auth.login, + 'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=expire), + 'iss': issuer + } + + token = jwt.encode(payload, secret, algorithm='HS256') + return token + +def access_token(request): + expire = get_config()['jwt']['token_expire'] + secret = get_config()['jwt']['secret'] + token = request.headers['Authorization'] + if not token.startswith("Bearer "): + raise HTTPBadRequest(reason='Unexpected bearer format') + token = token[7:] + decoded = verify_token(token) + if decoded: + decoded['exp'] = datetime.datetime.utcnow() + datetime.timedelta(seconds=expire) + token = jwt.encode(decoded, secret, algorithm='HS256') + return Response(text=str(token.decode('utf-8'))) + else: + return HTTPUnauthorized(reason='Token could not be refreshed') + return True + +def verify_token(token): + secret = get_config()['jwt']['secret'] + issuer = get_config()['jwt']['issuer'] + try: + decoded = jwt.decode(token, secret, issuer=issuer, algorithms=['HS256']) + except jwt.ExpiredSignatureError: + raise HTTPUnauthorized(reason='Token Expired') + except jwt.InvalidIssuerError: + raise HTTPUnauthorized(reason='Token could not be verified') + return decoded tiramisu = None diff --git a/src/risotto/services/user/user.py b/src/risotto/services/user/user.py index cab672a..6c074f7 100644 --- a/src/risotto/services/user/user.py +++ b/src/risotto/services/user/user.py @@ -13,6 +13,7 @@ class Risotto(Controller): """ pre-load servermodel and server """ user_login = get_config()['global']['admin_user'] + user_password = get_config()['global']['admin_user_password'] sql = ''' SELECT UserId FROM RisottoUser @@ -22,7 +23,8 @@ class Risotto(Controller): user_login) is None: await self._user_create(risotto_context, user_login, - user_login, + user_password, + user_login, user_login) await self._user_role_create(risotto_context, user_login, @@ -33,14 +35,16 @@ class Risotto(Controller): async def _user_create(self, risotto_context: Context, user_login: str, + user_password: str, user_name: str, user_surname: str) -> Dict: - user_insert = """INSERT INTO RisottoUser(UserLogin, UserName, UserSurname) - VALUES ($1,$2,$3) + user_insert = """INSERT INTO RisottoUser(UserLogin, UserPassword, UserName, UserSurname) + VALUES ($1,crypt($2, gen_salt('bf')),$3,$4) RETURNING UserId """ user_id = await risotto_context.connection.fetchval(user_insert, user_login, + user_password, user_name, user_surname) await self.call('v1.user.role.create', @@ -56,10 +60,12 @@ class Risotto(Controller): async def user_create(self, risotto_context: Context, user_login: str, + user_password: str, user_name: str, user_surname: str) -> Dict: return await self._user_create(risotto_context, user_login, + user_password, user_name, user_surname)