implement basicauth and jwt token

This commit is contained in:
Matthieu Lamalle 2020-01-24 13:43:14 +01:00
parent 52c878b0ab
commit d78d581c65
9 changed files with 116 additions and 15 deletions

View File

@ -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 ROLE risotto WITH LOGIN PASSWORD 'risotto';"
psql -U postgres -h localhost -c "CREATE DATABASE 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 "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 Gestion de la base de données avec Sqitch

View File

@ -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/rougail/src/rougail /usr/local/lib/python3.7
RUN ln -s /srv/src/risotto/src/risotto /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 RUN cd /srv/src/risotto && pip install -r requirements.txt
# Installation # Installation

View File

@ -6,6 +6,7 @@ services:
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
volumes: volumes:
- ../.:/srv/src/risotto - ../.:/srv/src/risotto
- ../messages:/usr/local/lib/messages
ports: ports:
- "8080:8080" - "8080:8080"
depends_on: depends_on:

View File

@ -7,6 +7,7 @@ psql --username "$POSTGRES_USER" <<-EOSQL
GRANT ALL ON DATABASE risotto TO risotto; GRANT ALL ON DATABASE risotto TO risotto;
\c risotto \c risotto
CREATE EXTENSION hstore; CREATE EXTENSION hstore;
CREATE EXTENSION pgcrypto;
EOSQL EOSQL
psql --username "risotto" --password "risotto" <<-EOSQL psql --username "risotto" --password "risotto" <<-EOSQL
@ -64,6 +65,7 @@ psql --username "risotto" --password "risotto" <<-EOSQL
CREATE TABLE RisottoUser ( CREATE TABLE RisottoUser (
UserId SERIAL PRIMARY KEY, UserId SERIAL PRIMARY KEY,
UserLogin VARCHAR(100) NOT NULL UNIQUE, UserLogin VARCHAR(100) NOT NULL UNIQUE,
UserPassword TEXT NOT NULL,
UserName VARCHAR(100) NOT NULL, UserName VARCHAR(100) NOT NULL,
UserSurname VARCHAR(100) NOT NULL UserSurname VARCHAR(100) NOT NULL
); );

View File

@ -11,6 +11,10 @@ parameters:
shortarg: l shortarg: l
description: Login de l'utilisateur. description: Login de l'utilisateur.
ref: User.Login ref: User.Login
user_password:
type: String
shortarg: p
description: Password de l'utilisateur.
user_name: user_name:
type: String type: String
shortarg: n shortarg: n

View File

@ -57,6 +57,7 @@ CREATE TABLE Server (
CREATE TABLE RisottoUser ( CREATE TABLE RisottoUser (
UserId SERIAL PRIMARY KEY, UserId SERIAL PRIMARY KEY,
UserLogin VARCHAR(100) NOT NULL UNIQUE, UserLogin VARCHAR(100) NOT NULL UNIQUE,
UserPassword TEXT NOT NULL,
UserName VARCHAR(100) NOT NULL, UserName VARCHAR(100) NOT NULL,
UserSurname VARCHAR(100) NOT NULL UserSurname VARCHAR(100) NOT NULL
); );

View File

@ -5,7 +5,12 @@ CONFIGURATION_DIR = 'configurations'
TEMPLATE_DIR = 'templates' TEMPLATE_DIR = 'templates'
TMP_DIR = 'tmp' TMP_DIR = 'tmp'
ROUGAIL_DTD_PATH = '../rougail/data/creole.dtd' 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 import os
from pathlib import PurePosixPath from pathlib import PurePosixPath
@ -19,16 +24,20 @@ def get_config():
'user': 'risotto', 'user': 'risotto',
'password': 'risotto', 'password': 'risotto',
}, },
'http_server': {'port': 8080, 'http_server': {'port': PORT,
#'default_user': "gnunux"},
'default_user': DEFAULT_USER}, 'default_user': DEFAULT_USER},
'global': {'message_root_path': CURRENT_PATH.parents[2] / 'messages', 'global': {'message_root_path': CURRENT_PATH.parents[2] / 'messages',
'debug': True, 'debug': True,
'internal_user': 'internal', 'internal_user': 'internal',
'check_role': True, 'check_role': True,
'rougail_dtd_path': '../rougail/data/creole.dtd', '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'}, '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}
} }

View File

@ -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 json import dumps
from traceback import print_exc from traceback import print_exc
from tiramisu import Config from tiramisu import Config
import datetime
import jwt
from .dispatcher import dispatcher from .dispatcher import dispatcher
from .utils import _ from .utils import _
@ -16,7 +18,17 @@ from .services import load_services
def create_context(request): def create_context(request):
risotto_context = Context() 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']) get_config()['http_server']['default_user'])
return risotto_context return risotto_context
@ -49,7 +61,7 @@ class extra_route_handler:
try: try:
returns = await cls.function(**kwargs) returns = await cls.function(**kwargs)
except NotAllowedError as err: except NotAllowedError as err:
raise HTTPNotFound(reason=str(err)) raise HTTPUnauthorized(reason=str(err))
except CallError as err: except CallError as err:
raise HTTPBadRequest(reason=str(err)) raise HTTPBadRequest(reason=str(err))
except Exception as err: except Exception as err:
@ -77,7 +89,7 @@ async def handle(request):
check_role=True, check_role=True,
**kwargs) **kwargs)
except NotAllowedError as err: except NotAllowedError as err:
raise HTTPNotFound(reason=str(err)) raise HTTPUnauthorized(reason=str(err))
except CallError as err: except CallError as err:
raise HTTPBadRequest(reason=str(err).replace('\n', ' ')) raise HTTPBadRequest(reason=str(err).replace('\n', ' '))
except Exception as err: except Exception as err:
@ -141,8 +153,73 @@ async def get_app(loop):
print() print()
del extra_routes del extra_routes
app.add_routes(routes) app.add_routes(routes)
app.router.add_post('/auth', auth)
app.router.add_post('/access_token', access_token)
await dispatcher.on_join() await dispatcher.on_join()
return await loop.create_server(app.make_handler(), '*', get_config()['http_server']['port']) 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 tiramisu = None

View File

@ -13,6 +13,7 @@ class Risotto(Controller):
""" pre-load servermodel and server """ pre-load servermodel and server
""" """
user_login = get_config()['global']['admin_user'] user_login = get_config()['global']['admin_user']
user_password = get_config()['global']['admin_user_password']
sql = ''' sql = '''
SELECT UserId SELECT UserId
FROM RisottoUser FROM RisottoUser
@ -22,7 +23,8 @@ class Risotto(Controller):
user_login) is None: user_login) is None:
await self._user_create(risotto_context, await self._user_create(risotto_context,
user_login, user_login,
user_login, user_password,
user_login,
user_login) user_login)
await self._user_role_create(risotto_context, await self._user_role_create(risotto_context,
user_login, user_login,
@ -33,14 +35,16 @@ class Risotto(Controller):
async def _user_create(self, async def _user_create(self,
risotto_context: Context, risotto_context: Context,
user_login: str, user_login: str,
user_password: str,
user_name: str, user_name: str,
user_surname: str) -> Dict: user_surname: str) -> Dict:
user_insert = """INSERT INTO RisottoUser(UserLogin, UserName, UserSurname) user_insert = """INSERT INTO RisottoUser(UserLogin, UserPassword, UserName, UserSurname)
VALUES ($1,$2,$3) VALUES ($1,crypt($2, gen_salt('bf')),$3,$4)
RETURNING UserId RETURNING UserId
""" """
user_id = await risotto_context.connection.fetchval(user_insert, user_id = await risotto_context.connection.fetchval(user_insert,
user_login, user_login,
user_password,
user_name, user_name,
user_surname) user_surname)
await self.call('v1.user.role.create', await self.call('v1.user.role.create',
@ -56,10 +60,12 @@ class Risotto(Controller):
async def user_create(self, async def user_create(self,
risotto_context: Context, risotto_context: Context,
user_login: str, user_login: str,
user_password: str,
user_name: str, user_name: str,
user_surname: str) -> Dict: user_surname: str) -> Dict:
return await self._user_create(risotto_context, return await self._user_create(risotto_context,
user_login, user_login,
user_password,
user_name, user_name,
user_surname) user_surname)