Merge branch 'dev' into rework-authenticator-system

This commit is contained in:
Alexandre Aubin 2021-06-13 15:34:22 +02:00
commit e2cb8cdfab
18 changed files with 271 additions and 385 deletions

View file

@ -45,13 +45,6 @@ Dev Documentation
https://moulinette.readthedocs.org https://moulinette.readthedocs.org
Requirements
------------
* Python 2.7
* python-bottle (>= 0.10)
* python-ldap (>= 2.4)
* PyYAML
Testing Testing
------- -------

60
debian/changelog vendored
View file

@ -1,8 +1,62 @@
moulinette (4.2) unstable; urgency=low moulinette (4.2.3.3) stable; urgency=low
- Placeholder for 4.2 to satisfy CI / debian build during dev - [fix] Damn array args bug (2c9ec9f6)
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 20 Jan 2021 05:19:58 +0100 Thanks to all contributors <3 ! (ljf)
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 03 Jun 2021 18:40:18 +0200
moulinette (4.2.3.2) stable; urgency=low
- [fix] wait 1s for message in call_async_output, prevent CPU overload ([#275](https://github.com/YunoHost/moulinette/pull/275))
- [i18n] Translations updated for Chinese (Simplified)
Thanks to all contributors <3 ! (Kayou, yahoo)
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 02 Jun 2021 20:23:31 +0200
moulinette (4.2.3.1) stable; urgency=low
- [fix] Request params not decoded ([#277](https://github.com/YunoHost/moulinette/pull/277))
Thanks to all contributors <3 ! (ljf)
-- Alexandre Aubin <alex.aubin@mailoo.org> Tue, 25 May 2021 18:59:01 +0200
moulinette (4.2.3) stable; urgency=low
- [fix] Unicode password doesn't log in ([#276](https://github.com/YunoHost/moulinette/pull/276))
- [i18n] Translations updated for Chinese (Simplified), Galician
Thanks to all contributors <3 ! (José M, ljf, yahoo)
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 24 May 2021 17:34:19 +0200
moulinette (4.2.2) stable; urgency=low
- [i18n] Translations updated for French, Hungarian
- Release as stable
Thanks to all contributors <3 ! (Dominik Blahó, Éric Gaspar)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sat, 08 May 2021 15:10:01 +0200
moulinette (4.2.1) testing; urgency=low
- Fix weird technical thing in actionmap sucategories loading, related to recent changes in Yunohost actionmap (135fae95)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sat, 17 Apr 2021 04:58:10 +0200
moulinette (4.2.0) testing; urgency=low
- [mod] Python2 -> python3 ([#228](https://github.com/YunoHost/moulinette/pull/228), 8e70561f, 570e5323, 3758b811, 90f894b5, [#269](https://github.com/YunoHost/moulinette/pull/269), e85b9f71, cafe68f3)
- [mod] Code formatting, test fixing, cleanup (677efcf6, 0de15467, [#268](https://github.com/YunoHost/moulinette/pull/268), affb54f8, f7199f7a, d6f82c91)
- [enh] Improve error semantic such that the webadmin shall be able to redirect to the proper log view ([#257](https://github.com/YunoHost/moulinette/pull/257), [#271](https://github.com/YunoHost/moulinette/pull/271))
- [fix] Simpler and more consistent logging initialization ([#263](https://github.com/YunoHost/moulinette/pull/263))
Thanks to all contributors <3 ! (Kay0u, Laurent Peuch)
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 19 Mar 2021 18:39:42 +0100
moulinette (4.1.4) stable; urgency=low moulinette (4.1.4) stable; urgency=low

View file

@ -1,181 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" License
Copyright (C) 2013 YunoHost
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses
"""
"""
Generate JSON specification files API
"""
import os
import sys
import yaml
import json
import requests
from yunohost import str_to_func, __version__
def main():
"""
"""
with open('action_map.yml') as f:
action_map = yaml.load(f)
try:
with open('/etc/yunohost/current_host', 'r') as f:
domain = f.readline().rstrip()
except IOError:
domain = requests.get('http://ip.yunohost.org').text
with open('action_map.yml') as f:
action_map = yaml.load(f)
resource_list = {
'apiVersion': __version__,
'swaggerVersion': '1.1',
'basePath': 'http://'+ domain + ':6767',
'apis': []
}
resources = {}
del action_map['general_arguments']
for category, category_params in action_map.items():
if 'category_help' not in category_params: category_params['category_help'] = ''
resource_path = '/api/'+ category
resource_list['apis'].append({
'path': resource_path,
'description': category_params['category_help']
})
resources[category] = {
'apiVersion': __version__,
'swaggerVersion': '1.1',
'basePath': 'http://'+ domain + ':6767',
'apis': []
}
resources[category]['resourcePath'] = resource_path
registered_paths = {}
for action, action_params in category_params['actions'].items():
if 'action_help' not in action_params:
action_params['action_help'] = ''
if 'api' not in action_params:
action_params['api'] = 'GET /'+ category +'/'+ action
method, path = action_params['api'].split(' ')
key_param = ''
if '{' in path:
key_param = path[path.find("{")+1:path.find("}")]
notes = ''
if str_to_func('yunohost_'+ category +'.'+ category +'_'+ action) is None:
notes = 'Not yet implemented'
operation = {
'httpMethod': method,
'nickname': category +'_'+ action,
'summary': action_params['action_help'],
'notes': notes,
'errorResponses': []
}
if 'arguments' in action_params:
operation['parameters'] = []
for arg_name, arg_params in action_params['arguments'].items():
if 'help' not in arg_params:
arg_params['help'] = ''
param_type = 'query'
allow_multiple = False
required = True
allowable_values = None
name = arg_name.replace('-', '_')
if name[0] == '_':
required = False
if 'full' in arg_params:
name = arg_params['full'][2:]
else:
name = name[2:]
name = name.replace('-', '_')
if 'nargs' in arg_params:
if arg_params['nargs'] == '*':
allow_multiple = True
required = False
if arg_params['nargs'] == '+':
allow_multiple = True
required = True
else:
allow_multiple = False
if 'choices' in arg_params:
allowable_values = {
'valueType': 'LIST',
'values': arg_params['choices']
}
if 'action' in arg_params and arg_params['action'] == 'store_true':
allowable_values = {
'valueType': 'LIST',
'values': ['true', 'false']
}
if name == key_param:
param_type = 'path'
required = True
allow_multiple = False
parameters = {
'paramType': param_type,
'name': name,
'description': arg_params['help'],
'dataType': 'string',
'required': required,
'allowMultiple': allow_multiple
}
if allowable_values is not None:
parameters['allowableValues'] = allowable_values
operation['parameters'].append(parameters)
if path in registered_paths:
resources[category]['apis'][registered_paths[path]]['operations'].append(operation)
resources[category]['apis'][registered_paths[path]]['description'] = ''
else:
registered_paths[path] = len(resources[category]['apis'])
resources[category]['apis'].append({
'path': path,
'description': action_params['action_help'],
'operations': [operation]
})
try: os.listdir(os.getcwd() +'/doc')
except OSError: os.makedirs(os.getcwd() +'/doc')
for category, api_dict in resources.items():
with open(os.getcwd() +'/doc/'+ category +'.json', 'w') as f:
json.dump(api_dict, f)
with open(os.getcwd() +'/doc/resources.json', 'w') as f:
json.dump(resource_list, f)
if __name__ == '__main__':
sys.exit(main())

View file

@ -1,114 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" License
Copyright (C) 2013 YunoHost
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses
"""
"""
Generate function header documentation
"""
import os
import sys
import yaml
import re
def main():
"""
"""
with open('action_map.yml') as f:
action_map = yaml.load(f)
resources = {}
del action_map['general_arguments']
for category, category_params in action_map.items():
if 'category_help' not in category_params: category_params['category_help'] = ''
with open('yunohost_'+ category +'.py', 'r') as f:
lines = f.readlines()
with open('yunohost_'+ category +'.py', 'w') as f:
in_block = False
for line in lines:
if in_block:
if re.search(r'^"""', line):
in_block = False
f.write('\n')
f.write(' '+ category_params['category_help'] +'\n')
f.write('"""\n')
else:
f.write(line)
if re.search(r'^""" yunohost_'+ category, line):
in_block = True
for action, action_params in category_params['actions'].items():
if 'action_help' not in action_params:
action_params['action_help'] = ''
help_lines = [
' """',
' '+ action_params['action_help'],
''
]
if 'arguments' in action_params:
help_lines.append(' Keyword argument:')
for arg_name, arg_params in action_params['arguments'].items():
if 'help' in arg_params:
help = ' -- '+ arg_params['help']
else:
help = ''
name = arg_name.replace('-', '_')
if name[0] == '_':
required = False
if 'full' in arg_params:
name = arg_params['full'][2:]
else:
name = name[2:]
name = name.replace('-', '_')
help_lines.append(' '+ name + help)
help_lines.append('')
help_lines.append(' """')
with open('yunohost_'+ category +'.py', 'r') as f:
lines = f.readlines()
with open('yunohost_'+ category +'.py', 'w') as f:
in_block = False
first_quotes = True
for line in lines:
if in_block:
if re.search(r'^ """', line):
if first_quotes:
first_quotes = False
else:
in_block = False
for help_line in help_lines:
f.write(help_line +'\n')
else:
f.write(line)
if re.search(r'^def '+ category +'_'+ action +'\(', line):
in_block = True
if __name__ == '__main__':
sys.exit(main())

View file

@ -4,14 +4,14 @@
"authentication_required": "需要验证", "authentication_required": "需要验证",
"authentication_required_long": "此操作需要验证", "authentication_required_long": "此操作需要验证",
"colon": "{} ", "colon": "{} ",
"confirm": "确认{prompt}", "confirm": "确认 {prompt}",
"deprecated_command": "{prog}{command}已经放弃使用,将来会删除", "deprecated_command": "{prog}{command}已经放弃使用,将来会删除",
"deprecated_command_alias": "{prog}{old}已经放弃使用,将来会删除,请使用{prog}{new}代替", "deprecated_command_alias": "{prog}{old}已经放弃使用,将来会删除,请使用{prog}{new}代替",
"error": "错误", "error": "错误:",
"error_see_log": "发生错误。请参看日志文件获取错误详情,日志文件位于 /var/log/yunohost/。", "error_see_log": "发生错误。请参看日志文件获取错误详情,日志文件位于 /var/log/yunohost/。",
"file_exists": "文件已存在{path}", "file_exists": "文件已存在: '{path}'",
"file_not_exist": "文件不存在{path}", "file_not_exist": "文件不存在: '{path}'",
"folder_exists": "目录已存在{path}", "folder_exists": "目录已存在: '{path}'",
"folder_not_exist": "目录不存在", "folder_not_exist": "目录不存在",
"info": "信息:", "info": "信息:",
"instance_already_running": "已经有一个YunoHost操作正在运行。 请等待它完成再运行另一个。", "instance_already_running": "已经有一个YunoHost操作正在运行。 请等待它完成再运行另一个。",

View file

@ -1,3 +1,58 @@
{ {
"password": "Heslo" "password": "Heslo",
"logged_out": "Jste odhlášen/a",
"ldap_server_is_down_restart_it": "LDAP služba neběží, probíhá pokus o její nastartování...",
"warn_the_user_that_lock_is_acquired": "Předchozí operace dokončena, nyní spouštíme tuto",
"warn_the_user_about_waiting_lock_again": "Stále čekáme...",
"warn_the_user_about_waiting_lock": "Jiná YunoHost operace právě probíhá, před spuštěním této čekáme na její dokončení",
"command_unknown": "Příkaz '{command:s}' neznámý?",
"download_bad_status_code": "{url:s} vrátil stavový kód {code:s}",
"download_unknown_error": "Chyba při stahování dat z {url:s}: {error:s}",
"download_timeout": "{url:s} příliš dlouho neodpovídá, akce přerušena.",
"download_ssl_error": "SSL chyba při spojení s {url:s}",
"invalid_url": "Špatný odkaz {url:s} (je vůbec dostupný?)",
"error_changing_file_permissions": "Chyba při nastavování oprávnění pro {path:s}: {error:s}",
"error_removing": "Chyba při přesunu {path:s}: {error:s}",
"error_writing_file": "Chyba při zápisu souboru/ů {file:s}: {error:s}",
"corrupted_toml": "Nepodařilo se načíst TOML z {ressource:s} (reason: {error:s})",
"corrupted_yaml": "Nepodařilo se načíst YAML z {ressource:s} (reason: {error:s})",
"corrupted_json": "Nepodařilo se načíst JSON {ressource:s} (reason: {error:s})",
"unknown_error_reading_file": "Vyskytla se neznámá chyba při čtení souboru/ů {file:s} (reason: {error:s})",
"cannot_write_file": "Nelze zapsat soubor/y {file:s} (reason: {error:s})",
"cannot_open_file": "Nelze otevřít soubor/y {file:s} (reason: {error:s})",
"websocket_request_expected": "Očekáván WebSocket požadavek",
"warning": "Varování:",
"values_mismatch": "Hodnoty nesouhlasí",
"unknown_user": "Neznámý '{user}' uživatel",
"unknown_group": "Neznámá '{group}' skupina",
"session_expired": "Sezení vypršelo. Přihlašte se znovu, prosím.",
"unable_retrieve_session": "Není možné obdržet sezení neboť '{exception}'",
"unable_authenticate": "Není možné ověřit",
"success": "Zadařilo se!",
"server_already_running": "Na tomto portu je server již provozován",
"root_required": "Pro provedení této akce musíte být root",
"pattern_not_match": "Neodpovídá výrazu",
"operation_interrupted": "Operace přerušena",
"not_logged_in": "Nejste přihlášen",
"logged_in": "Přihlášení",
"ldap_server_down": "Spojení s LDAP serverem se nezdařilo",
"ldap_attribute_already_exists": "Atribut '{attribute}' již obsahuje hodnotu '{value}'",
"invalid_usage": "Nesprávné použití, pass --help pro zobrazení nápovědy",
"invalid_token": "Nesprávný token - ověřte se prosím",
"invalid_password": "Nesprávné heslo",
"invalid_argument": "Nesprávný argument '{argument}': {error}",
"instance_already_running": "Právě probíhá jiná YunoHost operace. Před spuštěním další operace vyčkejte na její dokončení.",
"info": "Info:",
"folder_not_exist": "Adresář neexistuje",
"folder_exists": "Adresář již existuje: '{path}'",
"file_not_exist": "Soubor neexistuje: '{path}'",
"file_exists": "Soubor již existuje: '{path}'",
"error": "Chyba:",
"deprecated_command_alias": "'{prog} {old}' je zastaralý a bude odebrán v budoucích verzích, použijte '{prog} {new}'",
"deprecated_command": "'{prog} {command}' je zastaralý a bude odebrán v budoucích verzích",
"confirm": "Potvrdit {prompt}",
"colon": "{}: ",
"authentication_required_long": "K provedení této akce je vyžadováno ověření",
"authentication_required": "Vyžadováno ověření",
"argument_required": "Je vyžadován argument '{argument}'"
} }

1
locales/fi.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -56,6 +56,6 @@
"warn_the_user_about_waiting_lock_again": "Toujours en attente...", "warn_the_user_about_waiting_lock_again": "Toujours en attente...",
"warn_the_user_that_lock_is_acquired": "La commande précédente vient de se terminer, lancement de cette nouvelle commande", "warn_the_user_that_lock_is_acquired": "La commande précédente vient de se terminer, lancement de cette nouvelle commande",
"invalid_token": "Jeton non valide - veuillez vous authentifier", "invalid_token": "Jeton non valide - veuillez vous authentifier",
"ldap_server_is_down_restart_it": "Le service LDAP s'est arrêté, une tentative de redémarrage est en cours ...", "ldap_server_is_down_restart_it": "Le service LDAP s'est arrêté, une tentative de redémarrage est en cours...",
"session_expired": "La session a expiré. Merci de vous ré-authentifier." "session_expired": "La session a expiré. Merci de vous ré-authentifier."
} }

58
locales/gl.json Normal file
View file

@ -0,0 +1,58 @@
{
"ldap_attribute_already_exists": "O atributo '{attribute}' xa existe e ten o valor '{value}'",
"invalid_usage": "Uso non válido, pass --help para ver a axuda",
"invalid_token": "Token non válido - por favor autentícate",
"invalid_password": "Contrasinal non válido",
"invalid_argument": "Argumento non válido '{argument}': {error}",
"instance_already_running": "Hai unha operación de YunoHost en execución. Por favor agarda a que remate antes de realizar unha nova.",
"info": "Info:",
"folder_not_exist": "O cartafol non existe",
"folder_exists": "Xa existe o cartafol: '{path}'",
"file_not_exist": "Non existe o ficheiro: '{path}'",
"file_exists": "Xa existe o ficheiro: '{path}'",
"error": "Erro:",
"deprecated_command_alias": "'{prog} {old}' xa non se utiliza e será eliminado no futuro, usa '{prog} {new}' no seu lugar",
"deprecated_command": "'{prog} {command}' xa non se utiliza e xa non se usará no futuro",
"confirm": "Confirma {prompt}",
"colon": "{}: ",
"authentication_required_long": "Requírese autenticación para realizar esta acción",
"authentication_required": "Autenticación requerida",
"argument_required": "O argumento '{argument}' é requerido",
"logged_out": "Sesión pechada",
"password": "Contrasinal",
"warning": "Aviso:",
"values_mismatch": "Non concordan os valores",
"unknown_user": "Usuaria '{user}' descoñecida",
"unknown_group": "Grupo '{group}' descoñecido",
"session_expired": "A sesión caducou. Volve a conectar por favor.",
"unable_retrieve_session": "Non se puido obter a sesión porque '{exception}'",
"unable_authenticate": "Non se puido autenticar",
"success": "Ben feito!",
"server_already_running": "Xa hai un servidor a funcionar nese porto",
"root_required": "Tes que ser root para facer esta acción",
"pattern_not_match": "Non concorda co patrón",
"operation_interrupted": "Interrumpeuse a operación",
"not_logged_in": "Non estás conectada",
"logged_in": "Conectada",
"ldap_server_down": "Non se puido acadar o servidor LDAP",
"ldap_server_is_down_restart_it": "O servizo LDAP está caído, intentando reinicialo...",
"warn_the_user_that_lock_is_acquired": "O outro comando rematou, agora executarase este",
"warn_the_user_about_waiting_lock_again": "Agardando...",
"warn_the_user_about_waiting_lock": "Estase executando outro comando de YunoHost neste intre, estamos agardando a que remate para executar este",
"command_unknown": "Comando '{command:s}' descoñecido?",
"download_bad_status_code": "{url:s} devolveu o código de estado {code:s}",
"download_unknown_error": "Erro ao descargar os datos desde {url:s}: {error:s}",
"download_timeout": "{url:s} está tardando en responder, deixámolo.",
"download_ssl_error": "Erro SSL ao conectar con {url:s}",
"invalid_url": "URL non válido {url:s} (existe esta web?)",
"error_changing_file_permissions": "Erro ao cambiar os permisos de {path:s}: {error:s}",
"error_removing": "Erro ao eliminar {path:s}: {error:s}",
"error_writing_file": "Erro ao escribir o ficheiro {file:s}: {error:s}",
"corrupted_toml": "Lectura corrupta de datos TOML de {ressource:s} (razón: {error:s})",
"corrupted_yaml": "Lectura corrupta dos datos YAML de {ressource:s} (razón: {error:s})",
"corrupted_json": "Lectura corrupta dos datos JSON de {ressource:s} (razón: {error:s})",
"unknown_error_reading_file": "Erro descoñecido ao intentar ler o ficheiro {file:s} (razón: {error:s})",
"cannot_write_file": "Non se puido escribir o ficheiro {file:s} (razón: {error:s})",
"cannot_open_file": "Non se puido abrir o ficheiro {file:s} (razón: {error:s})",
"websocket_request_expected": "Agardábase unha solicitude WebSocket"
}

View file

@ -6,14 +6,14 @@
"colon": "{}: ", "colon": "{}: ",
"confirm": "पुष्टि करें {prompt}", "confirm": "पुष्टि करें {prompt}",
"deprecated_command": "'{prog}' '{command}' का प्रयोग न करे, भविष्य में इसे हटा दिया जाएगा", "deprecated_command": "'{prog}' '{command}' का प्रयोग न करे, भविष्य में इसे हटा दिया जाएगा",
"deprecated_command_alias": "'{prog} {old}' का प्रयोग न करे ,भविष्य में इसे हटा दिया जाएगा।इस की जगह '{prog} {new}' का प्रोयोग करे।", "deprecated_command_alias": "'{prog} {old}' अब पुराना हो गया है और इसे भविष्य में हटा दिया जाएगा, इस की जगह '{prog} {new}' का प्रयोग करें",
"error": "गलती:", "error": "गलती:",
"error_see_log": "एक त्रुटि पाई गई। कृपया विवरण के लिए लॉग देखें।", "error_see_log": "एक त्रुटि पाई गई। कृपया विवरण के लिए लॉग देखें।",
"file_exists": "फ़ाइल पहले से ही मौजूद है:'{path}'", "file_exists": "फ़ाइल पहले से ही मौजूद है:'{path}'",
"file_not_exist": "फ़ाइल मौजूद नहीं है: '{path}'", "file_not_exist": "फ़ाइल मौजूद नहीं है: '{path}'",
"folder_exists": "फ़ोल्डर में पहले से ही मौजूद है: '{path}'", "folder_exists": "फ़ोल्डर में पहले से ही मौजूद है: '{path}'",
"folder_not_exist": "फ़ोल्डर मौजूद नहीं है", "folder_not_exist": "फ़ोल्डर मौजूद नहीं है",
"instance_already_running": "आवृत्ति पहले से चल रही है।", "instance_already_running": "यूनोहोस्ट का एक कार्य पहले से चल रहा है। कृपया इस कार्य के समाप्त होने का इंतज़ार करें।",
"invalid_argument": "अवैध तर्क '{argument}':'{error}'", "invalid_argument": "अवैध तर्क '{argument}':'{error}'",
"invalid_password": "अवैध पासवर्ड", "invalid_password": "अवैध पासवर्ड",
"invalid_usage": "अवैध उपयोग, सहायता देखने के लिए --help साथ लिखे।", "invalid_usage": "अवैध उपयोग, सहायता देखने के लिए --help साथ लिखे।",
@ -35,5 +35,6 @@
"unknown_user": "अज्ञात उपयोगकर्ता: '{user}'", "unknown_user": "अज्ञात उपयोगकर्ता: '{user}'",
"values_mismatch": "वैल्यूज मेल नहीं खाती।", "values_mismatch": "वैल्यूज मेल नहीं खाती।",
"warning": "चेतावनी:", "warning": "चेतावनी:",
"websocket_request_expected": "एक WebSocket अनुरोध की उम्मीद।" "websocket_request_expected": "एक WebSocket अनुरोध की उम्मीद।",
"info": "सूचना:"
} }

View file

@ -1,4 +1,19 @@
{ {
"logged_out": "Kilépett", "logged_out": "Kilépett",
"password": "Jelszó" "password": "Jelszó",
"download_timeout": "{url:s} régóta nem válaszol, folyamat megszakítva.",
"invalid_url": "Helytelen URL: {url:s} (biztos létezik az oldal?)",
"cannot_open_file": "{file:s} megnyitása sikertelen (Oka: {error:s})",
"unknown_user": "Ismeretlen felhasználó: '{user}'",
"unknown_group": "Ismeretlen csoport: '{group}'",
"server_already_running": "Egy szerver már fut ezen a porton",
"logged_in": "Bejelentkezve",
"success": "Siker!",
"values_mismatch": "Eltérő értékek",
"warning": "Figyelem:",
"invalid_password": "Helytelen jelszó",
"info": "Információ:",
"file_not_exist": "A fájl nem létezik: '{path}'",
"file_exists": "A fájl már létezik: '{path}'",
"error": "Hiba:"
} }

View file

@ -1,10 +1,10 @@
{ {
"logged_out": "Wylogowano", "logged_out": "Wylogowano",
"password": "Hasło", "password": "Hasło",
"warn_the_user_that_lock_is_acquired": "drugie polecenie właśnie się zakończyło, teraz uruchamiając to polecenie", "warn_the_user_that_lock_is_acquired": "Inne polecenie właśnie się zakończyło, teraz uruchamiam to polecenie",
"warn_the_user_about_waiting_lock_again": "Wciąż czekam...", "warn_the_user_about_waiting_lock_again": "Wciąż czekam...",
"warn_the_user_about_waiting_lock": "Kolejne polecenie YunoHost jest teraz uruchomione, czekamy na jego zakończenie przed uruchomieniem tego", "warn_the_user_about_waiting_lock": "Kolejne polecenie YunoHost jest teraz uruchomione, czekamy na jego zakończenie przed uruchomieniem tego",
"command_unknown": "Polecenie „{command:s}” jest nieznane?", "command_unknown": "Polecenie '{command:s}' jest nieznane?",
"download_bad_status_code": "{url:s} zwrócił kod stanu {code:s}", "download_bad_status_code": "{url:s} zwrócił kod stanu {code:s}",
"download_unknown_error": "Błąd podczas pobierania danych z {url:s}: {error:s}", "download_unknown_error": "Błąd podczas pobierania danych z {url:s}: {error:s}",
"download_timeout": "{url:s} odpowiedział zbyt długo, poddał się.", "download_timeout": "{url:s} odpowiedział zbyt długo, poddał się.",
@ -13,17 +13,17 @@
"error_changing_file_permissions": "Błąd podczas zmiany uprawnień dla {path:s}: {error:s}", "error_changing_file_permissions": "Błąd podczas zmiany uprawnień dla {path:s}: {error:s}",
"error_removing": "Błąd podczas usuwania {path:s}: {error:s}", "error_removing": "Błąd podczas usuwania {path:s}: {error:s}",
"error_writing_file": "Błąd podczas zapisywania pliku {file:s}: {error:s}", "error_writing_file": "Błąd podczas zapisywania pliku {file:s}: {error:s}",
"corrupted_toml": "Uszkodzony toml z {ressource: s} (powód: {error:s})", "corrupted_toml": "Uszkodzony TOML z {ressource: s} (reason: {error:s})",
"corrupted_yaml": "Uszkodzony yaml odczytany z {ressource:s} (powód: {error:s})", "corrupted_yaml": "Uszkodzony YAML odczytany z {ressource:s} (reason: {error:s})",
"corrupted_json": "Uszkodzony json odczytany z {ressource:s} (powód: {error:s})", "corrupted_json": "Uszkodzony JSON odczytany z {ressource:s} (reason: {error:s})",
"unknown_error_reading_file": "Nieznany błąd podczas próby odczytania pliku {file:s} (przyczyna: {error:s})", "unknown_error_reading_file": "Nieznany błąd podczas próby odczytania pliku {file:s} (przyczyna: {error:s})",
"cannot_write_file": "Nie można zapisać pliku {file:s} (przyczyna: {error:s})", "cannot_write_file": "Nie można zapisać pliku {file:s} (przyczyna: {error:s})",
"cannot_open_file": "Nie można otworzyć pliku {file:s} (przyczyna: {error:s})", "cannot_open_file": "Nie można otworzyć pliku {file:s} (przyczyna: {error:s})",
"websocket_request_expected": "Oczekiwano żądania WebSocket", "websocket_request_expected": "Oczekiwano żądania WebSocket",
"warning": "Ostrzeżenie:", "warning": "Ostrzeżenie:",
"values_mismatch": "Wartości nie pasują", "values_mismatch": "Wartości nie pasują",
"unknown_user": "Nieznany użytkownik „{user}”", "unknown_user": "Nieznany użytkownik '{user}'",
"unknown_group": "Nieznana grupa „{group}”", "unknown_group": "Nieznana grupa '{group}'",
"unable_retrieve_session": "Nie można pobrać sesji, ponieważ „{exception}”", "unable_retrieve_session": "Nie można pobrać sesji, ponieważ „{exception}”",
"unable_authenticate": "Nie można uwierzytelnić", "unable_authenticate": "Nie można uwierzytelnić",
"success": "Sukces!", "success": "Sukces!",
@ -53,5 +53,7 @@
"colon": "{}: ", "colon": "{}: ",
"authentication_required_long": "Do wykonania tej czynności wymagane jest uwierzytelnienie", "authentication_required_long": "Do wykonania tej czynności wymagane jest uwierzytelnienie",
"authentication_required": "Wymagane uwierzytelnienie", "authentication_required": "Wymagane uwierzytelnienie",
"argument_required": "Argument „{argument}” jest wymagany" "argument_required": "Argument „{argument}” jest wymagany",
"ldap_server_is_down_restart_it": "Usługa LDAP nie działa, próba restartu...",
"session_expired": "Sesja wygasła. Zaloguj się ponownie."
} }

View file

@ -14,7 +14,12 @@ from importlib import import_module
from moulinette import m18n, msignals from moulinette import m18n, msignals
from moulinette.cache import open_cachefile from moulinette.cache import open_cachefile
from moulinette.globals import init_moulinette_env from moulinette.globals import init_moulinette_env
from moulinette.core import MoulinetteError, MoulinetteLock from moulinette.core import (
MoulinetteError,
MoulinetteLock,
MoulinetteAuthenticationError,
MoulinetteValidationError,
)
from moulinette.interfaces import BaseActionsMapParser, GLOBAL_SECTION, TO_RETURN_PROP from moulinette.interfaces import BaseActionsMapParser, GLOBAL_SECTION, TO_RETURN_PROP
from moulinette.utils.log import start_action_logging from moulinette.utils.log import start_action_logging
@ -207,7 +212,9 @@ class PatternParameter(_ExtraParameter):
if msg == message: if msg == message:
msg = m18n.g(message) msg = m18n.g(message)
raise MoulinetteError("invalid_argument", argument=arg_name, error=msg) raise MoulinetteValidationError(
"invalid_argument", argument=arg_name, error=msg
)
return arg_value return arg_value
@staticmethod @staticmethod
@ -238,7 +245,7 @@ class RequiredParameter(_ExtraParameter):
def __call__(self, required, arg_name, arg_value): def __call__(self, required, arg_name, arg_value):
if required and (arg_value is None or arg_value == ""): if required and (arg_value is None or arg_value == ""):
logger.warning("argument '%s' is required", arg_name) logger.warning("argument '%s' is required", arg_name)
raise MoulinetteError("argument_required", argument=arg_name) raise MoulinetteValidationError("argument_required", argument=arg_name)
return arg_value return arg_value
@staticmethod @staticmethod
@ -491,7 +498,7 @@ class ActionsMap(object):
authenticator = self.get_authenticator(auth_method) authenticator = self.get_authenticator(auth_method)
if not msignals.authenticate(authenticator): if not msignals.authenticate(authenticator):
raise MoulinetteError("authentication_required_long") raise MoulinetteAuthenticationError("authentication_required_long")
def process(self, args, timeout=None, **kwargs): def process(self, args, timeout=None, **kwargs):
""" """
@ -771,6 +778,9 @@ class ActionsMap(object):
# No parser for the action # No parser for the action
continue continue
if action_parser is None: # No parser for the action
continue
# Store action identifier and add arguments # Store action identifier and add arguments
action_parser.set_defaults(_tid=tid) action_parser.set_defaults(_tid=tid)
action_parser.add_arguments( action_parser.add_arguments(

View file

@ -6,7 +6,7 @@ import hashlib
import hmac import hmac
from moulinette.cache import open_cachefile, get_cachedir, cachefile_exists from moulinette.cache import open_cachefile, get_cachedir, cachefile_exists
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError, MoulinetteAuthenticationError
logger = logging.getLogger("moulinette.authenticator") logger = logging.getLogger("moulinette.authenticator")
@ -80,7 +80,7 @@ class BaseAuthenticator(object):
raise raise
except Exception as e: except Exception as e:
logger.exception("authentication {self.name} failed because '{e}'") logger.exception("authentication {self.name} failed because '{e}'")
raise MoulinetteError("unable_authenticate") raise MoulinetteAuthenticationError("unable_authenticate")
else: else:
is_authenticated = True is_authenticated = True
@ -109,7 +109,7 @@ class BaseAuthenticator(object):
raise raise
except Exception as e: except Exception as e:
logger.exception("authentication {self.name} failed because '{e}'") logger.exception("authentication {self.name} failed because '{e}'")
raise MoulinetteError("unable_authenticate") raise MoulinetteAuthenticationError("unable_authenticate")
else: else:
is_authenticated = True is_authenticated = True
@ -117,7 +117,7 @@ class BaseAuthenticator(object):
# No credentials given, can't authenticate # No credentials given, can't authenticate
# #
else: else:
raise MoulinetteError("unable_authenticate") raise MoulinetteAuthenticationError("unable_authenticate")
self.is_authenticated = is_authenticated self.is_authenticated = is_authenticated
return is_authenticated return is_authenticated
@ -146,7 +146,7 @@ class BaseAuthenticator(object):
def _authenticate_session(self, session_id, session_token): def _authenticate_session(self, session_id, session_token):
"""Checks session and token against the stored session token""" """Checks session and token against the stored session token"""
if not self._session_exists(session_id): if not self._session_exists(session_id):
raise MoulinetteError("session_expired") raise MoulinetteAuthenticationError("session_expired")
try: try:
# FIXME : shouldn't we also add a check that this session file # FIXME : shouldn't we also add a check that this session file
# is not too old ? e.g. not older than 24 hours ? idk... # is not too old ? e.g. not older than 24 hours ? idk...
@ -155,7 +155,7 @@ class BaseAuthenticator(object):
stored_hash = f.read() stored_hash = f.read()
except IOError as e: except IOError as e:
logger.debug("unable to retrieve session", exc_info=1) logger.debug("unable to retrieve session", exc_info=1)
raise MoulinetteError("unable_retrieve_session", exception=e) raise MoulinetteAuthenticationError("unable_retrieve_session", exception=e)
else: else:
# #
# session_id (or just id) : This is unique id for the current session from the user. Not too important # session_id (or just id) : This is unique id for the current session from the user. Not too important
@ -177,7 +177,7 @@ class BaseAuthenticator(object):
hash_ = hashlib.sha256(to_hash).hexdigest() hash_ = hashlib.sha256(to_hash).hexdigest()
if not hmac.compare_digest(hash_, stored_hash): if not hmac.compare_digest(hash_, stored_hash):
raise MoulinetteError("invalid_token") raise MoulinetteAuthenticationError("invalid_token")
else: else:
return return

View file

@ -370,6 +370,8 @@ class MoulinetteSignals(object):
class MoulinetteError(Exception): class MoulinetteError(Exception):
http_code = 500
"""Moulinette base exception""" """Moulinette base exception"""
def __init__(self, key, raw_msg=False, *args, **kwargs): def __init__(self, key, raw_msg=False, *args, **kwargs):
@ -384,6 +386,16 @@ class MoulinetteError(Exception):
return self.strerror return self.strerror
class MoulinetteValidationError(MoulinetteError):
http_code = 400
class MoulinetteAuthenticationError(MoulinetteError):
http_code = 401
class MoulinetteLock(object): class MoulinetteLock(object):
"""Locker for a moulinette instance """Locker for a moulinette instance

View file

@ -15,7 +15,7 @@ from bottle import abort
from moulinette import msignals, m18n, env from moulinette import msignals, m18n, env
from moulinette.actionsmap import ActionsMap from moulinette.actionsmap import ActionsMap
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError, MoulinetteValidationError
from moulinette.interfaces import ( from moulinette.interfaces import (
BaseActionsMapParser, BaseActionsMapParser,
BaseInterface, BaseInterface,
@ -207,8 +207,7 @@ class _HTTPArgumentParser(object):
return self._parser.dequeue_callbacks(*args, **kwargs) return self._parser.dequeue_callbacks(*args, **kwargs)
def _error(self, message): def _error(self, message):
# TODO: Raise a proper exception raise MoulinetteValidationError(message, raw_msg=True)
raise MoulinetteError(message, raw_msg=True)
class _ActionsMapPlugin(object): class _ActionsMapPlugin(object):
@ -250,9 +249,9 @@ class _ActionsMapPlugin(object):
def wrapper(): def wrapper():
kwargs = {} kwargs = {}
try: try:
kwargs["password"] = request.POST["password"] kwargs["password"] = request.POST.password
except KeyError: except KeyError:
raise HTTPBadRequestResponse("Missing password parameter") raise HTTPResponse("Missing password parameter", 400)
kwargs["profile"] = request.POST.get("profile", self.actionsmap.default_authentication) kwargs["profile"] = request.POST.get("profile", self.actionsmap.default_authentication)
return callback(**kwargs) return callback(**kwargs)
@ -321,7 +320,7 @@ class _ActionsMapPlugin(object):
for a in args: for a in args:
params[a] = True params[a] = True
# Append other request params # Append other request params
for k, v in request.params.dict.items(): for k, v in request.params.decode().dict.items():
v = _format(v) v = _format(v)
if k not in params.keys(): if k not in params.keys():
params[k] = v params[k] = v
@ -387,7 +386,7 @@ class _ActionsMapPlugin(object):
self.logout(profile) self.logout(profile)
except: except:
pass pass
raise HTTPUnauthorizedResponse(e.strerror) raise HTTPResponse(e.strerror, 401)
else: else:
# Update dicts with new values # Update dicts with new values
s_tokens[profile] = s_new_token s_tokens[profile] = s_new_token
@ -420,7 +419,7 @@ class _ActionsMapPlugin(object):
if profile not in request.get_cookie( if profile not in request.get_cookie(
"session.tokens", secret=s_secret, default={} "session.tokens", secret=s_secret, default={}
): ):
raise HTTPUnauthorizedResponse(m18n.g("not_logged_in")) raise HTTPResponse(m18n.g("not_logged_in"), 401)
else: else:
del self.secrets[s_id] del self.secrets[s_id]
authenticator = self.actionsmap.get_authenticator(profile) authenticator = self.actionsmap.get_authenticator(profile)
@ -448,7 +447,7 @@ class _ActionsMapPlugin(object):
wsock = request.environ.get("wsgi.websocket") wsock = request.environ.get("wsgi.websocket")
if not wsock: if not wsock:
raise HTTPErrorResponse(m18n.g("websocket_request_expected")) raise HTTPResponse(m18n.g("websocket_request_expected"), 500)
while True: while True:
item = queue.get() item = queue.get()
@ -485,7 +484,7 @@ class _ActionsMapPlugin(object):
try: try:
ret = self.actionsmap.process(arguments, timeout=30, route=_route) ret = self.actionsmap.process(arguments, timeout=30, route=_route)
except MoulinetteError as e: except MoulinetteError as e:
raise HTTPBadRequestResponse(e) raise moulinette_error_to_http_response(e)
except Exception as e: except Exception as e:
if isinstance(e, HTTPResponse): if isinstance(e, HTTPResponse):
raise e raise e
@ -493,7 +492,7 @@ class _ActionsMapPlugin(object):
tb = traceback.format_exc() tb = traceback.format_exc()
logs = {"route": _route, "arguments": arguments, "traceback": tb} logs = {"route": _route, "arguments": arguments, "traceback": tb}
return HTTPErrorResponse(json_encode(logs)) return HTTPResponse(json_encode(logs), 500)
else: else:
return format_for_response(ret) return format_for_response(ret)
finally: finally:
@ -521,7 +520,7 @@ class _ActionsMapPlugin(object):
] ]
except KeyError: except KeyError:
msg = m18n.g("authentication_required") msg = m18n.g("authentication_required")
raise HTTPUnauthorizedResponse(msg) raise HTTPResponse(msg, 401)
else: else:
return authenticator(token=(s_id, s_token)) return authenticator(token=(s_id, s_token))
@ -548,36 +547,17 @@ class _ActionsMapPlugin(object):
# HTTP Responses ------------------------------------------------------- # HTTP Responses -------------------------------------------------------
class HTTPOKResponse(HTTPResponse): def moulinette_error_to_http_response(error):
def __init__(self, output=""):
super(HTTPOKResponse, self).__init__(output, 200)
content = error.content()
class HTTPBadRequestResponse(HTTPResponse): if isinstance(content, dict):
def __init__(self, error=""): return HTTPResponse(
json_encode(content),
if isinstance(error, MoulinetteError): error.http_code,
content = error.content() headers={"Content-type": "application/json"},
if isinstance(content, dict): )
super(HTTPBadRequestResponse, self).__init__( else:
json_encode(content), return HTTPResponse(content, error.http_code)
400,
headers={"Content-type": "application/json"},
)
else:
super(HTTPBadRequestResponse, self).__init__(content, 400)
else:
super(HTTPBadRequestResponse, self).__init__(error, 400)
class HTTPUnauthorizedResponse(HTTPResponse):
def __init__(self, output=""):
super(HTTPUnauthorizedResponse, self).__init__(output, 401)
class HTTPErrorResponse(HTTPResponse):
def __init__(self, output=""):
super(HTTPErrorResponse, self).__init__(output, 500)
def format_for_response(content): def format_for_response(content):
@ -692,7 +672,7 @@ class ActionsMapParser(BaseActionsMapParser):
except KeyError as e: except KeyError as e:
error_message = "no argument parser found for route '%s': %s" % (route, e) error_message = "no argument parser found for route '%s': %s" % (route, e)
logger.error(error_message) logger.error(error_message)
raise MoulinetteError(error_message, raw_msg=True) raise MoulinetteValidationError(error_message, raw_msg=True)
return parser.authentication return parser.authentication
@ -709,7 +689,7 @@ class ActionsMapParser(BaseActionsMapParser):
except KeyError as e: except KeyError as e:
error_message = "no argument parser found for route '%s': %s" % (route, e) error_message = "no argument parser found for route '%s': %s" % (route, e)
logger.error(error_message) logger.error(error_message)
raise MoulinetteError(error_message, raw_msg=True) raise MoulinetteValidationError(error_message, raw_msg=True)
ret = argparse.Namespace() ret = argparse.Namespace()
# TODO: Catch errors? # TODO: Catch errors?

View file

@ -13,7 +13,7 @@ import argcomplete
from moulinette import msignals, m18n from moulinette import msignals, m18n
from moulinette.actionsmap import ActionsMap from moulinette.actionsmap import ActionsMap
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError, MoulinetteValidationError
from moulinette.interfaces import ( from moulinette.interfaces import (
BaseActionsMapParser, BaseActionsMapParser,
BaseInterface, BaseInterface,
@ -411,7 +411,7 @@ class ActionsMapParser(BaseActionsMapParser):
e, e,
) )
logger.exception(error_message) logger.exception(error_message)
raise MoulinetteError(error_message, raw_msg=True) raise MoulinetteValidationError(error_message, raw_msg=True)
tid = getattr(ret, "_tid", None) tid = getattr(ret, "_tid", None)
@ -443,7 +443,7 @@ class ActionsMapParser(BaseActionsMapParser):
e, e,
) )
logger.exception(error_message) logger.exception(error_message)
raise MoulinetteError(error_message, raw_msg=True) raise MoulinetteValidationError(error_message, raw_msg=True)
else: else:
self.prepare_action_namespace(getattr(ret, "_tid", None), ret) self.prepare_action_namespace(getattr(ret, "_tid", None), ret)
self._parser.dequeue_callbacks(ret) self._parser.dequeue_callbacks(ret)
@ -494,7 +494,7 @@ class Interface(BaseInterface):
""" """
if output_as and output_as not in ["json", "plain", "none"]: if output_as and output_as not in ["json", "plain", "none"]:
raise MoulinetteError("invalid_usage") raise MoulinetteValidationError("invalid_usage")
# auto-complete # auto-complete
argcomplete.autocomplete(self.actionsmap.parser._parser) argcomplete.autocomplete(self.actionsmap.parser._parser)
@ -558,7 +558,7 @@ class Interface(BaseInterface):
if confirm: if confirm:
m = message[0].lower() + message[1:] m = message[0].lower() + message[1:]
if prompt(m18n.g("confirm", prompt=m)) != value: if prompt(m18n.g("confirm", prompt=m)) != value:
raise MoulinetteError("values_mismatch") raise MoulinetteValidationError("values_mismatch")
return value return value

View file

@ -77,7 +77,7 @@ def call_async_output(args, callback, **kwargs):
while True: while True:
try: try:
callback, message = log_queue.get_nowait() callback, message = log_queue.get(True, 1)
except queue.Empty: except queue.Empty:
break break