Compare commits

..

5 Commits
docker ... jwt

Author SHA1 Message Date
424273360d verify audience in jwt 2020-01-30 15:46:18 +01:00
ae6dfb2644 add audience claim data to jwt 2020-01-24 15:07:30 +01:00
d78d581c65 implement basicauth and jwt token 2020-01-24 13:43:14 +01:00
52c878b0ab replace all zephir occurence 2020-01-23 14:05:07 +01:00
5666c01bdc update docker 2020-01-23 11:01:38 +01:00
41 changed files with 157 additions and 51 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:
@ -13,8 +14,8 @@ services:
links: links:
- postgres - postgres
#command: tail -F /var/log #command: tail -F /var/log
command: python /srv/src/risotto/script/server.py command: python -u /srv/src/risotto/script/server.py
restart: unless-stopped restart: on-failure
postgres: postgres:
image: postgres:11-alpine image: postgres:11-alpine
environment: environment:

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

@ -5,7 +5,7 @@ description: |
Retourne des informations sur la session HTTP courante de l'utilisateur. Retourne des informations sur la session HTTP courante de l'utilisateur.
sampleuse: | sampleuse: |
zephir-client identity.session-user.get cucchiaiata identity.session-user.get
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Retourne les préférences de l'utilisateur spécifié. Retourne les préférences de l'utilisateur spécifié.
sampleuse: | sampleuse: |
zephir-client identity.settings.get -u yo cucchiaiata identity.settings.get -u yo
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Enregistre des préférences pour l'utilisateur spécifié. Enregistre des préférences pour l'utilisateur spécifié.
sampleuse: | sampleuse: |
zephir-client identity.settings.set -u yo cucchiaiata identity.settings.set -u yo
pattern: rpc pattern: rpc

View File

@ -11,7 +11,7 @@ pattern: rpc
domain: server-domain domain: server-domain
sampleuse: | sampleuse: |
zephir-client server.delete -s 1 cucchiaiata server.delete -s 1
parameters: parameters:
serverid: serverid:

View File

@ -5,7 +5,7 @@ description: |
Transmet une commande à exécuter sur un serveur donné. Transmet une commande à exécuter sur un serveur donné.
sampleuse: | sampleuse: |
zephir-client server.exec.command -s 1 -c reconfigure cucchiaiata server.exec.command -s 1 -c reconfigure
domain: server-domain domain: server-domain

View File

@ -5,7 +5,7 @@ description: |
Déploie la configuration sur un serveur donné. Déploie la configuration sur un serveur donné.
sampleuse: | sampleuse: |
zephir-client server.exec.deploy -s 1 cucchiaiata server.exec.deploy -s 1
domain: server-domain domain: server-domain

View File

@ -5,7 +5,7 @@ description: |
Liste les commandes exécuté pour un identifiant de tâche. Liste les commandes exécuté pour un identifiant de tâche.
sampleuse: | sampleuse: |
zephir-client server.exec.list -j 1 cucchiaiata server.exec.list -j 1
domain: execution-domain domain: execution-domain

View File

@ -5,7 +5,7 @@ description: |
Liste les commandes exécutées sur un serveur donné. Liste les commandes exécutées sur un serveur donné.
sampleuse: | sampleuse: |
zephir-client server.exec.list -s 1 cucchiaiata server.exec.list -s 1
domain: execution-domain domain: execution-domain

View File

@ -5,7 +5,7 @@ description: |
Retourne la liste des sélections de serveurs d'un serveur Retourne la liste des sélections de serveurs d'un serveur
sampleuse: | sampleuse: |
zephir-client server.serverselection.list cucchiaiata server.serverselection.list
pattern: rpc pattern: rpc

View File

@ -7,7 +7,7 @@ description: |
public: true public: true
sampleuse: | sampleuse: |
zephir-client server.update -s 1 -n toto -d "server description" cucchiaiata server.update -s 1 -n toto -d "server description"
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Associe un service applicatif à un modèle de serveur. Associe un service applicatif à un modèle de serveur.
sampleuse: | sampleuse: |
zephir-client servermodel.applicationservice.join -m 1 -s 1 cucchiaiata servermodel.applicationservice.join -m 1 -s 1
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Retourne la liste des applications service. Retourne la liste des applications service.
sampleuse: | sampleuse: |
zephir-client servermodel.applicationservice.list -s 6 cucchiaiata servermodel.applicationservice.list -s 6
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Crée un modèle de serveur. Crée un modèle de serveur.
sampleuse: | sampleuse: |
zephir-client servermodel.create -p 1 -n "MonServeurModele" -d "Ma description" -s 1 cucchiaiata servermodel.create -p 1 -n "MonServeurModele" -d "Ma description" -s 1
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Retourne la liste des subreleases. Retourne la liste des subreleases.
sampleuse: | sampleuse: |
zephir-client servermodel.subrelease.list cucchiaiata servermodel.subrelease.list
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Crée un sélection de serveurs. Crée un sélection de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.create -n Select1 -d "Ma description" cucchiaiata serverselection.create -n Select1 -d "Ma description"
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Supprime une sélection de serveurs. Supprime une sélection de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.delete -s 1 cucchiaiata serverselection.delete -s 1
pattern: rpc pattern: rpc

View File

@ -6,7 +6,7 @@ description: |
sampleuse: | sampleuse: |
zephir-client serverselection.describe -s 1 cucchiaiata serverselection.describe -s 1
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Transmet une commande à exécuter sur une sélection de serveurs. Transmet une commande à exécuter sur une sélection de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.exec.command -s 1 -c reconfigure cucchiaiata serverselection.exec.command -s 1 -c reconfigure
domain: server-domain domain: server-domain

View File

@ -5,7 +5,7 @@ description: |
Déploie la configuration sur les serveurs d'une sélection de serveurs. Déploie la configuration sur les serveurs d'une sélection de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.exec.deploy -s 1 cucchiaiata serverselection.exec.deploy -s 1
domain: server-domain domain: server-domain

View File

@ -5,7 +5,7 @@ description: |
Retourne la liste des sélections de serveurs. Retourne la liste des sélections de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.list cucchiaiata serverselection.list
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Ajoute un serveur à une sélection de serveurs. Ajoute un serveur à une sélection de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.add.server -s 1 -i 1 cucchiaiata serverselection.add.server -s 1 -i 1
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Supprime un serveur d'une sélection de serveurs. Supprime un serveur d'une sélection de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.remove.server -s 1 -i 1 cucchiaiata serverselection.remove.server -s 1 -i 1
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Renseigne une liste de serveur dans une sélection de serveurs. Renseigne une liste de serveur dans une sélection de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.server.set -s 1 -i 1 cucchiaiata serverselection.server.set -s 1 -i 1
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Modifie une sélection de serveur. Modifie une sélection de serveur.
sampleuse: | sampleuse: |
zephir-client serverselection.update -s 1 -n Select1 -d "Ma description" cucchiaiata serverselection.update -s 1 -n Select1 -d "Ma description"
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Associe un utilisateur à une sélection de serveurs. Associe un utilisateur à une sélection de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.add.user -s 1 -u yo -r admin cucchiaiata serverselection.add.user -s 1 -u yo -r admin
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Retourne la sélection de serveurs par défaut de l'utilisateur. Retourne la sélection de serveurs par défaut de l'utilisateur.
sampleuse: | sampleuse: |
zephir-client serverselection.user.default cucchiaiata serverselection.user.default
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Retourne les sélections de serveurs dont l'utilisateur fait parti. Retourne les sélections de serveurs dont l'utilisateur fait parti.
sampleuse: | sampleuse: |
zephir-client serverselection.user.list cucchiaiata serverselection.user.list
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Dissocie un utilisateur d'une sélection de serveurs. Dissocie un utilisateur d'une sélection de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.remove.user -s 1 -u yo cucchiaiata serverselection.remove.user -s 1 -u yo
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Retourne le rôle d'utilisateur sur une selection de serveurs. Retourne le rôle d'utilisateur sur une selection de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.user.role.get -d '{}' cucchiaiata serverselection.user.role.get -d '{}'
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Retourne le rôle d'un utlisateur sur un serveur. Retourne le rôle d'un utlisateur sur un serveur.
sampleuse: | sampleuse: |
zephir-client serverselection.user.role.server.get -d '{}' cucchiaiata serverselection.user.role.server.get -d '{}'
pattern: rpc pattern: rpc

View File

@ -5,7 +5,7 @@ description: |
Modifie le rôle d'un utilisateur pour une sélection de serveurs. Modifie le rôle d'un utilisateur pour une sélection de serveurs.
sampleuse: | sampleuse: |
zephir-client serverselection.update.user -s 1 -u yo -r admin cucchiaiata serverselection.update.user -s 1 -u yo -r admin
pattern: rpc pattern: rpc

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,13 @@ 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
JWT_TOKEN_AUDIENCE = "Risotto"
import os import os
from pathlib import PurePosixPath from pathlib import PurePosixPath
@ -19,16 +25,21 @@ 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,
'audience': JWT_TOKEN_AUDIENCE}
} }

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,6 +18,16 @@ from .services import load_services
def create_context(request): def create_context(request):
risotto_context = Context() risotto_context = Context()
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', 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,76 @@ 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']
audience = get_config()['jwt']['audience']
payload = {
'user': auth.login,
'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=expire),
'iss': issuer,
'aud': audience
}
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 verified')
def verify_token(token):
secret = get_config()['jwt']['secret']
issuer = get_config()['jwt']['issuer']
audience = get_config()['jwt']['audience']
try:
decoded = jwt.decode(token, secret, issuer=issuer, audience=audience, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
raise HTTPUnauthorized(reason='Token Expired')
except jwt.InvalidIssuerError:
raise HTTPUnauthorized(reason='Token could not be verified')
except jwt.InvalidAudienceError:
raise HTTPUnauthorized(reason='Token audience not match')
return decoded
tiramisu = None tiramisu = None

View File

@ -38,7 +38,7 @@ class AnyOption(Option):
class MessageDefinition: class MessageDefinition:
""" """
A MessageDefinition is a representation of a message in the Zephir application messaging context A MessageDefinition is a representation of a message in the Risotto application messaging context
""" """
__slots__ = ('version', __slots__ = ('version',
'uri', 'uri',
@ -135,7 +135,7 @@ class ParameterDefinition:
class ResponseDefinition: class ResponseDefinition:
""" """
An ResponseDefinition is a representation of a response in the Zephir application messaging context An ResponseDefinition is a representation of a response in the Risotto application messaging context
""" """
__slots__ = ('description', __slots__ = ('description',
'type', 'type',
@ -184,7 +184,7 @@ class ResponseDefinition:
class ErrorDefinition: class ErrorDefinition:
""" """
An ErrorDefinition is a representation of an error in the Zephir application messaging context An ErrorDefinition is a representation of an error in the Risotto application messaging context
""" """
__slots__ = ('uri',) __slots__ = ('uri',)

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,6 +23,7 @@ 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_password,
user_login, user_login,
user_login) user_login)
await self._user_role_create(risotto_context, await self._user_role_create(risotto_context,
@ -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)