Merge branch 'stretch-unstable' into sasl_authentication

This commit is contained in:
Alexandre Aubin 2019-05-02 15:20:33 +02:00
commit ff6343c53a
33 changed files with 531 additions and 200 deletions

60
debian/changelog vendored
View file

@ -1,3 +1,63 @@
moulinette (3.5.2) stable; urgency=low
- Release as stable !
- [fix] Do not miserably crash if the lock does not exist when attempting to release it
- [i18n] Update translation for Arabic, Italian
Thanks to all contributors (Aleks, BoF, silkevicious) <3 !
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 10 Apr 2019 02:14:00 +0000
moulinette (3.5.1) testing; urgency=low
* [fix] Fix case where stdinfo is not provided in call_async_output (0a300e5)
* [i18n] Improve translation for Greek, Hungarian, Polish, Swedish, French, Catalan, Occitan
Thanks to all contributors (Aleks, ariasuni, Quenti, ppr, Xaloc) <3 !
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 03 Apr 2019 02:25:00 +0000
moulinette (3.5.0) testing; urgency=low
* [i18n] Improve Russian and Chinese (Mandarin) translations
Contributors : n3uz, Алексей
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 13 Mar 2019 17:20:00 +0000
moulinette (3.4.2) stable; urgency=low
* [i18n] Improve Basque translation
Thanks to all contributors (A. Garaialde) <3 !
-- Alexandre Aubin <alex.aubin@mailoo.org> Tue, 29 Jan 2019 16:50:00 +0000
moulinette (3.4.1) testing; urgency=low
* [i18n] Improve Chinese(Mandarin) translation
* [i18n] Misc orthotypograhy
Thanks to all contributors (Jibec, aleiyer) <3 !
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 17 Jan 2019 21:50:00 +0000
moulinette (3.4.0) testing; urgency=low
* [fix] Code cleaning with autopep8 (#187)
* [fix] Automatically reconnect LDAP authenticator when slapd restarts (#185)
* [enh] Display date as system timezone (#184)
* [mod] Make sure `gpg.encrypt` actually does something (#182)
* [mod] Add possiblity to get attribute name of conflict in LDAP (#181)
* [enh] Simplify moulinette error management (#180)
* [mod] Remove Access-Control-Allow-Origin (#174)
* [enh] Protect against CSRF (#171)
* [i18n] Improve Spanish translation
Thanks to all contributors (Aleks, randomstuff, irina11y, Josue, gdayon, ljf) <3 !
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 20 Dec 2018 21:53:00 +0000
moulinette (3.3.1) stable; urgency=low moulinette (3.3.1) stable; urgency=low
* [fix] 'force' semantics in 'utils.filesystem.mkdir' (#177) * [fix] 'force' semantics in 'utils.filesystem.mkdir' (#177)

3
debian/control vendored
View file

@ -16,7 +16,8 @@ Depends: ${misc:Depends}, ${python:Depends},
python-gnupg, python-gnupg,
python-gevent-websocket, python-gevent-websocket,
python-argcomplete, python-argcomplete,
python-psutil python-psutil,
python-tz
Replaces: yunohost-cli Replaces: yunohost-cli
Breaks: yunohost-cli Breaks: yunohost-cli
Description: prototype interfaces with ease in Python Description: prototype interfaces with ease in Python

View file

@ -45,10 +45,11 @@
"error_removing": "خطأ أثناء عملية حذف {path:s}: {error:s}", "error_removing": "خطأ أثناء عملية حذف {path:s}: {error:s}",
"error_changing_file_permissions": "خطأ أثناء عملية تعديل التصريحات لـ {path:s}: {error:s}", "error_changing_file_permissions": "خطأ أثناء عملية تعديل التصريحات لـ {path:s}: {error:s}",
"invalid_url": "خطأ في عنوان الرابط {url:s} (هل هذا الموقع موجود حقًا ؟)", "invalid_url": "خطأ في عنوان الرابط {url:s} (هل هذا الموقع موجود حقًا ؟)",
"download_ssl_error": "خطأ في الإتصال الآمن عبر الـ SSL أثناء محاولة الإتصال بـ {url:s}", "download_ssl_error": "خطأ في الاتصال الآمن عبر الـ SSL أثناء محاولة الربط بـ {url:s}",
"download_timeout": "{url:s} استغرق مدة طويلة جدا للإستجابة، فتوقّف.", "download_timeout": "{url:s} استغرق مدة طويلة جدا للإستجابة، فتوقّف.",
"download_unknown_error": "خطأ أثناء عملية تنزيل البيانات مِن {url:s} : {error:s}", "download_unknown_error": "خطأ أثناء عملية تنزيل البيانات مِن {url:s} : {error:s}",
"download_bad_status_code": "{url:s} أعاد رمز الحالة {code:s}", "download_bad_status_code": "{url:s} أعاد رمز الحالة {code:s}",
"command_unknown": "الأمر '{command:s}' غير معروف ؟", "command_unknown": "الأمر '{command:s}' غير معروف ؟",
"corrupted_yaml": "قراءة مُشوّهة لنسق yaml مِن {ressource:s} (السبب : {error:s})" "corrupted_yaml": "قراءة مُشوّهة لنسق yaml مِن {ressource:s} (السبب : {error:s})",
"info": "معلومة:"
} }

1
locales/bn_BD.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -50,5 +50,6 @@
"download_timeout": "{url:s} ha tardat massa en respondre, s'ha deixat d'esperar.", "download_timeout": "{url:s} ha tardat massa en respondre, s'ha deixat d'esperar.",
"download_unknown_error": "Error al baixar dades des de {url:s}: {error:s}", "download_unknown_error": "Error al baixar dades des de {url:s}: {error:s}",
"download_bad_status_code": "{url:s} ha retornat el codi d'estat {code:s}", "download_bad_status_code": "{url:s} ha retornat el codi d'estat {code:s}",
"command_unknown": "Ordre '{command:s}' desconegut ?" "command_unknown": "Ordre '{command:s}' desconegut ?",
"info": "Info:"
} }

54
locales/cmn.json Normal file
View file

@ -0,0 +1,54 @@
{
"argument_required": "{argument}是必须的",
"authentication_profile_required": "必须验证配置文件{profile}",
"authentication_required": "需要验证",
"authentication_required_long": "此操作需要验证",
"colon": "{} ",
"confirm": "确认{prompt}",
"deprecated_command": "{prog}{command}已经放弃使用,将来会删除",
"deprecated_command_alias": "{prog}{old}已经放弃使用,将来会删除,请使用{prog}{new}代替",
"error": "错误:",
"error_see_log": "发生错误。请参看日志文件获取错误详情,日志文件位于 /var/log/yunohost/。",
"file_exists": "文件已存在:{path}",
"file_not_exist": "文件不存在:{path}",
"folder_exists": "目录已存在:{path}",
"folder_not_exist": "目录不存在",
"info": "信息:",
"instance_already_running": "实例已正在运行",
"invalid_argument": "参数错误{argument}{error}",
"invalid_password": "密码错误",
"invalid_usage": "用法错误,输入 --help 查看帮助信息",
"ldap_attribute_already_exists": "参数{attribute}已赋值{value}",
"ldap_operation_error": "LDAP操作时发生了错误",
"ldap_server_down": "无法连接LDAP服务器",
"logged_in": "登录成功",
"logged_out": "注销成功",
"not_logged_in": "您未登录",
"operation_interrupted": "操作中断",
"password": "密码",
"pattern_not_match": "模式匹配失败",
"root_required": "必须以root身份进行此操作",
"server_already_running": "服务已运行在指定端口",
"success": "成功!",
"unable_authenticate": "认证失败",
"unable_retrieve_session": "获取会话失败",
"unknown_group": "未知组{group}",
"unknown_user": "未知用户{user}",
"values_mismatch": "值不匹配",
"warning": "警告:",
"websocket_request_expected": "期望一个WebSocket请求",
"cannot_open_file": "不能打开文件{file:s}(原因:{error:s}",
"cannot_write_file": "写入文件{file:s}失败(原因:{error:s}",
"unknown_error_reading_file": "尝试读取文件{file:s}时发生错误",
"corrupted_json": "json数据{ressource:s}读取失败(原因:{error:s}",
"corrupted_yaml": "读取yaml文件{ressource:s}失败(原因:{error:s}",
"error_writing_file": "写入文件{file:s}失败:{error:s}",
"error_removing": "删除路径{path:s}失败:{error:s}",
"error_changing_file_permissions": "目录{path:s}权限修改失败:{error:s}",
"invalid_url": "url{url:s}无效site是否存在",
"download_ssl_error": "连接{url:s}时发生SSL错误",
"download_timeout": "{url:s}响应超时,放弃。",
"download_unknown_error": "下载{url:s}失败:{error:s}",
"download_bad_status_code": "{url:s}返回状态码:{code:s}",
"command_unknown": "未知命令:{command:s}"
}

1
locales/el.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -27,7 +27,6 @@
"operation_interrupted": "Operation interrupted", "operation_interrupted": "Operation interrupted",
"password": "Password", "password": "Password",
"pattern_not_match": "Does not match pattern", "pattern_not_match": "Does not match pattern",
"permission_denied": "Permission denied",
"root_required": "You must be root to perform this action", "root_required": "You must be root to perform this action",
"server_already_running": "A server is already running on that port", "server_already_running": "A server is already running on that port",
"success": "Success!", "success": "Success!",
@ -47,10 +46,10 @@
"error_writing_file": "Error when writing file {file:s}: {error:s}", "error_writing_file": "Error when writing file {file:s}: {error:s}",
"error_removing": "Error when removing {path:s}: {error:s}", "error_removing": "Error when removing {path:s}: {error:s}",
"error_changing_file_permissions": "Error when changing permissions for {path:s}: {error:s}", "error_changing_file_permissions": "Error when changing permissions for {path:s}: {error:s}",
"invalid_url": "Invalid url {url:s} (does this site exists ?)", "invalid_url": "Invalid url {url:s} (does this site exists?)",
"download_ssl_error": "SSL error when connecting to {url:s}", "download_ssl_error": "SSL error when connecting to {url:s}",
"download_timeout": "{url:s} took too long to answer, gave up.", "download_timeout": "{url:s} took too long to answer, gave up.",
"download_unknown_error": "Error when downloading data from {url:s} : {error:s}", "download_unknown_error": "Error when downloading data from {url:s}: {error:s}",
"download_bad_status_code": "{url:s} returned status code {code:s}", "download_bad_status_code": "{url:s} returned status code {code:s}",
"command_unknown": "Command '{command:s}' unknown ?" "command_unknown": "Command '{command:s}' unknown?"
} }

View file

@ -49,5 +49,6 @@
"download_timeout": "{url:s} tardó demasiado en responder, me rindo.", "download_timeout": "{url:s} tardó demasiado en responder, me rindo.",
"download_unknown_error": "Error al descargar datos desde {url:s} : {error:s}", "download_unknown_error": "Error al descargar datos desde {url:s} : {error:s}",
"download_bad_status_code": "{url:s} devolvió el código de estado {code:s}", "download_bad_status_code": "{url:s} devolvió el código de estado {code:s}",
"command_unknown": "Comando '{command:s}' desconocido ?" "command_unknown": "Comando '{command:s}' desconocido ?",
"corrupted_yaml": "yaml corrupto leido desde {ressource:s} (motivo: {error:s})"
} }

3
locales/eu.json Normal file
View file

@ -0,0 +1,3 @@
{
"argument_required": "'{argument}' argumentua beharrezkoa da"
}

View file

@ -1,23 +1,23 @@
{ {
"argument_required": "Largument « {argument} » est requis", "argument_required": "Largument '{argument}' est requis",
"authentication_profile_required": "Lauthentification au profil « {profile} » requise", "authentication_profile_required": "Lauthentification au profil '{profile}' est requise",
"authentication_required": "Authentification requise", "authentication_required": "Authentification requise",
"authentication_required_long": "Lauthentification est requise pour exécuter cette action", "authentication_required_long": "Lauthentification est requise pour exécuter cette action",
"colon": "{} : ", "colon": "{} : ",
"confirm": "Confirmez : {prompt}", "confirm": "Confirmez : {prompt}",
"deprecated_command": "« {prog} {command} » est déprécié et sera bientôt supprimé", "deprecated_command": "'{prog} {command}' est déprécié et sera bientôt supprimé",
"deprecated_command_alias": "« {prog} {old} » est déprécié et sera bientôt supprimé, utilisez « {prog} {new} » à la place", "deprecated_command_alias": "'{prog} {old}' est déprécié et sera bientôt supprimé, utilisez '{prog} {new}' à la place",
"error": "Erreur :", "error": "Erreur :",
"error_see_log": "Une erreur est survenue. Veuillez consulter les journaux pour plus de détails, ils sont situés en /var/log/yunohost/.", "error_see_log": "Une erreur est survenue. Veuillez consulter les journaux pour plus de détails, ils sont situés dans /var/log/yunohost/.",
"file_exists": "Le fichier existe déjà : « {path} »", "file_exists": "Le fichier existe déjà : '{path}'",
"file_not_exist": "Le fichier « {path} » nexiste pas", "file_not_exist": "Le fichier '{path}' nexiste pas",
"folder_exists": "Le dossier existe déjà : « {path} »", "folder_exists": "Le dossier existe déjà : '{path}'",
"folder_not_exist": "Le dossier nexiste pas", "folder_not_exist": "Le dossier nexiste pas",
"instance_already_running": "Une instance est déjà en cours dexécution", "instance_already_running": "Une instance est déjà en cours dexécution",
"invalid_argument": "Argument « {argument} » incorrect : {error}", "invalid_argument": "Argument '{argument}' incorrect : {error}",
"invalid_password": "Mot de passe incorrect", "invalid_password": "Mot de passe incorrect",
"invalid_usage": "Utilisation erronée, utilisez --help pour accéder à laide", "invalid_usage": "Utilisation erronée, utilisez --help pour accéder à laide",
"ldap_attribute_already_exists": "Lattribut « {attribute} » existe déjà avec comme valeur : {value}", "ldap_attribute_already_exists": "Lattribut '{attribute}' existe déjà avec la valeur suivante : '{value}'",
"ldap_operation_error": "Une erreur est survenue lors de lopération LDAP", "ldap_operation_error": "Une erreur est survenue lors de lopération LDAP",
"ldap_server_down": "Impossible datteindre le serveur LDAP", "ldap_server_down": "Impossible datteindre le serveur LDAP",
"logged_in": "Connecté", "logged_in": "Connecté",
@ -32,23 +32,24 @@
"success": "Succès !", "success": "Succès !",
"unable_authenticate": "Impossible de vous authentifier", "unable_authenticate": "Impossible de vous authentifier",
"unable_retrieve_session": "Impossible de récupérer la session", "unable_retrieve_session": "Impossible de récupérer la session",
"unknown_group": "Groupe « {group} » inconnu", "unknown_group": "Groupe '{group}' inconnu",
"unknown_user": "Utilisateur « {user} » inconnu", "unknown_user": "L'utilisateur « {user} » est inconnu",
"values_mismatch": "Les valeurs ne correspondent pas", "values_mismatch": "Les valeurs ne correspondent pas",
"warning": "Attention :", "warning": "Attention :",
"websocket_request_expected": "Requête WebSocket attendue", "websocket_request_expected": "Une requête WebSocket est attendue",
"cannot_open_file": "Impossible douvrir le fichier {file:s} (cause : {error:s})", "cannot_open_file": "Impossible douvrir le fichier {file:s} (raison : {error:s})",
"cannot_write_file": "Ne peut pas écrire le fichier {file:s} (cause : {error:s})", "cannot_write_file": "Ne peut pas écrire le fichier {file:s} (raison : {error:s})",
"unknown_error_reading_file": "Erreur inconnue en essayant de lire le fichier {file:s}", "unknown_error_reading_file": "Erreur inconnue en essayant de lire le fichier {file:s}",
"corrupted_json": "Json corrompu lu depuis {ressource:s} (cause : {error:s})", "corrupted_json": "Fichier JSON corrompu en lecture depuis {ressource:s} (raison : {error:s})",
"error_writing_file": "Erreur en écrivant le fichier {file:s}:{error:s}", "error_writing_file": "Erreur en écrivant le fichier {file:s} : {error:s}",
"error_removing": "Erreur lors de la suppression {path:s}:{error:s}", "error_removing": "Erreur lors de la suppression {path:s} : {error:s}",
"error_changing_file_permissions": "Erreur lors de la modification des autorisations pour {path:s}:{error:s}", "error_changing_file_permissions": "Erreur lors de la modification des autorisations pour {path:s} : {error:s}",
"invalid_url": "Url invalide {url:s} (ce site existe-t-il ?)", "invalid_url": "URL {url:s} invalide : ce site existe-t-il ?",
"download_ssl_error": "Erreur SSL lors de la connexion à {url:s}", "download_ssl_error": "Erreur SSL lors de la connexion à {url:s}",
"download_timeout": "{url:s} a pris trop de temps pour répondre, abandon.", "download_timeout": "{url:s} a pris trop de temps pour répondre : abandon.",
"download_unknown_error": "Erreur lors du téléchargement des données à partir de {url:s}:{error:s}", "download_unknown_error": "Erreur lors du téléchargement des données à partir de {url:s} : {error:s}",
"download_bad_status_code": "{url:s} code de statut renvoyé {code:s}", "download_bad_status_code": "{url:s} renvoie le code d'état {code:s}",
"command_unknown": "Commande « {command:s} » inconnue ?", "command_unknown": "Commande '{command:s}' inconnue ?",
"corrupted_yaml": "YAML corrompu lu {ressource:s} depuis (cause : {error:s})" "corrupted_yaml": "Fichier YAML corrompu en lecture depuis {ressource:s} (raison : {error:s})",
"info": "Info :"
} }

1
locales/hu.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -50,5 +50,6 @@
"download_timeout": "{url:s} ci ha messo troppo a rispondere, abbandonato.", "download_timeout": "{url:s} ci ha messo troppo a rispondere, abbandonato.",
"download_unknown_error": "Errore durante il download di dati da {url:s} : {error:s}", "download_unknown_error": "Errore durante il download di dati da {url:s} : {error:s}",
"download_bad_status_code": "{url:s} ha restituito il codice di stato {code:s}", "download_bad_status_code": "{url:s} ha restituito il codice di stato {code:s}",
"command_unknown": "Comando '{command:s}' sconosciuto ?" "command_unknown": "Comando '{command:s}' sconosciuto ?",
"info": "Info:"
} }

View file

@ -50,5 +50,6 @@
"download_bad_status_code": "{url:s} tòrna lo còdi destat {code:s}", "download_bad_status_code": "{url:s} tòrna lo còdi destat {code:s}",
"command_unknown": "Comanda {command:s} desconeguda?", "command_unknown": "Comanda {command:s} desconeguda?",
"corrupted_json": "Fichièr Json corromput legit de {ressource:s} (rason: {error:s})", "corrupted_json": "Fichièr Json corromput legit de {ressource:s} (rason: {error:s})",
"corrupted_yaml": "Fichièr YAML corromput legit de {ressource:s} (rason: {error:s})" "corrupted_yaml": "Fichièr YAML corromput legit de {ressource:s} (rason: {error:s})",
"info": "Info:"
} }

1
locales/pl.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -1 +1,48 @@
{} {
"argument_required": "Требуется'{argument}' аргумент",
"authentication_profile_required": "Для доступа к '{profile}' требуется аутентификация",
"authentication_required": "Требуется аутентификация",
"authentication_required_long": "Для этого действия требуется аутентификация",
"colon": "{}: ",
"confirm": "Подтвердить {prompt}",
"deprecated_command": "'{prog} {command}' устарела и будет удалена",
"deprecated_command_alias": "'{prog} {old}' устарела и будет удалена, вместо неё используйте '{prog} {new}'",
"error": "Ошибка:",
"error_see_log": "Произошла ошибка. Пожалуйста, смотри подробности в логах, находящихся /var/log/yunohost/.",
"file_exists": "Файл уже существует: '{path}'",
"file_not_exist": "Файл не существует: '{path}'",
"folder_exists": "Каталог уже существует: '{path}'",
"folder_not_exist": "Каталог не существует",
"invalid_argument": "Неправильный аргумент '{argument}': {error}",
"invalid_password": "Неправильный пароль",
"ldap_attribute_already_exists": "Атрибут '{attribute}' уже существует со значением '{value}'",
"logged_in": "Вы вошли",
"logged_out": "Вы вышли из системы",
"not_logged_in": "Вы не залогинились",
"operation_interrupted": "Действие прервано",
"password": "Пароль",
"pattern_not_match": "Не соответствует образцу",
"server_already_running": "Сервер уже запущен на этом порте",
"success": "Отлично!",
"unable_authenticate": "Аутентификация невозможна",
"unknown_group": "Неизвестная '{group}' группа",
"unknown_user": "Неизвестный '{user}' пользователь",
"values_mismatch": "Неверные значения",
"warning": "Внимание :",
"websocket_request_expected": "Ожидается запрос WebSocket",
"cannot_open_file": "Не могу открыть файл {file:s} (причина: {error:s})",
"cannot_write_file": "Не могу записать файл {file:s} (причина: {error:s})",
"unknown_error_reading_file": "Неизвестная ошибка при чтении файла {file:s}",
"corrupted_yaml": "Повреждённой yaml получен от {ressource:s} (причина: {error:s})",
"error_writing_file": "Ошибка при записи файла {file:s}: {error:s}",
"error_removing": "Ошибка при удалении {path:s}: {error:s}",
"invalid_url": "Неправильный url {url:s} (этот сайт существует ?)",
"download_ssl_error": "Ошибка SSL при соединении с {url:s}",
"download_timeout": "Превышено время ожидания ответа от {url:s}.",
"download_unknown_error": "Ошибка при загрузке данных с {url:s} : {error:s}",
"instance_already_running": "Процесс уже запущен",
"ldap_operation_error": "Ошибка в процессе работы LDAP",
"root_required": "Чтобы выполнить это действие, вы должны иметь права root",
"corrupted_json": "Повреждённый json получен от {ressource:s} (причина: {error:s})",
"command_unknown": "Команда '{command:s}' неизвестна ?"
}

1
locales/sv.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -137,5 +137,5 @@ def cli(namespaces, args, use_cache=True, output_as=None,
except MoulinetteError as e: except MoulinetteError as e:
import logging import logging
logging.getLogger(namespaces[0]).error(e.strerror) logging.getLogger(namespaces[0]).error(e.strerror)
return e.errno return 1
return 0 return 0

View file

@ -2,7 +2,6 @@
import os import os
import re import re
import errno
import logging import logging
import yaml import yaml
import cPickle as pickle import cPickle as pickle
@ -26,6 +25,7 @@ logger = logging.getLogger('moulinette.actionsmap')
# Extra parameters definition # Extra parameters definition
class _ExtraParameter(object): class _ExtraParameter(object):
""" """
Argument parser for an extra parameter. Argument parser for an extra parameter.
@ -105,6 +105,7 @@ class CommentParameter(_ExtraParameter):
class AskParameter(_ExtraParameter): class AskParameter(_ExtraParameter):
""" """
Ask for the argument value if possible and needed. Ask for the argument value if possible and needed.
@ -139,6 +140,7 @@ class AskParameter(_ExtraParameter):
class PasswordParameter(AskParameter): class PasswordParameter(AskParameter):
""" """
Ask for the password argument value if possible and needed. Ask for the password argument value if possible and needed.
@ -160,6 +162,7 @@ class PasswordParameter(AskParameter):
class PatternParameter(_ExtraParameter): class PatternParameter(_ExtraParameter):
""" """
Check if the argument value match a pattern. Check if the argument value match a pattern.
@ -187,9 +190,8 @@ class PatternParameter(_ExtraParameter):
if msg == message: if msg == message:
msg = m18n.g(message) msg = m18n.g(message)
raise MoulinetteError(errno.EINVAL, raise MoulinetteError('invalid_argument',
m18n.g('invalid_argument', argument=arg_name, error=msg)
argument=arg_name, error=msg))
return arg_value return arg_value
@staticmethod @staticmethod
@ -206,6 +208,7 @@ class PatternParameter(_ExtraParameter):
class RequiredParameter(_ExtraParameter): class RequiredParameter(_ExtraParameter):
""" """
Check if a required argument is defined or not. Check if a required argument is defined or not.
@ -218,9 +221,8 @@ class RequiredParameter(_ExtraParameter):
if required and (arg_value is None or arg_value == ''): if required and (arg_value is None or arg_value == ''):
logger.debug("argument '%s' is required", logger.debug("argument '%s' is required",
arg_name) arg_name)
raise MoulinetteError(errno.EINVAL, raise MoulinetteError('argument_required',
m18n.g('argument_required', argument=arg_name)
argument=arg_name))
return arg_value return arg_value
@staticmethod @staticmethod
@ -243,6 +245,7 @@ extraparameters_list = [CommentParameter, AskParameter, PasswordParameter,
class ExtraArgumentParser(object): class ExtraArgumentParser(object):
""" """
Argument validator and parser for the extra parameters. Argument validator and parser for the extra parameters.
@ -285,7 +288,7 @@ class ExtraArgumentParser(object):
except Exception as e: except Exception as e:
logger.error("unable to validate extra parameter '%s' " logger.error("unable to validate extra parameter '%s' "
"for argument '%s': %s", p, arg_name, e) "for argument '%s': %s", p, arg_name, e)
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
return parameters return parameters
@ -360,6 +363,7 @@ def ordered_yaml_load(stream):
class ActionsMap(object): class ActionsMap(object):
"""Validate and process actions defined into an actions map """Validate and process actions defined into an actions map
The actions map defines the features - and their usage - of an The actions map defines the features - and their usage - of an
@ -501,7 +505,7 @@ class ActionsMap(object):
except (AttributeError, ImportError): except (AttributeError, ImportError):
logger.exception("unable to load function %s.%s", logger.exception("unable to load function %s.%s",
namespace, func_name) namespace, func_name)
raise MoulinetteError(errno.EIO, m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
else: else:
log_id = start_action_logging() log_id = start_action_logging()
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):

View file

@ -1,10 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import errno
import gnupg import gnupg
import logging import logging
from moulinette import m18n
from moulinette.cache import open_cachefile from moulinette.cache import open_cachefile
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
@ -14,6 +12,7 @@ logger = logging.getLogger('moulinette.authenticator')
# Base Class ----------------------------------------------------------- # Base Class -----------------------------------------------------------
class BaseAuthenticator(object): class BaseAuthenticator(object):
"""Authenticator base representation """Authenticator base representation
Each authenticators must implement an Authenticator class derived Each authenticators must implement an Authenticator class derived
@ -97,7 +96,7 @@ class BaseAuthenticator(object):
except TypeError: except TypeError:
logger.error("unable to extract token parts from '%s'", token) logger.error("unable to extract token parts from '%s'", token)
if password is None: if password is None:
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
logger.info("session will not be stored") logger.info("session will not be stored")
store_session = False store_session = False
@ -114,7 +113,7 @@ class BaseAuthenticator(object):
except: except:
logger.exception("authentication (name: '%s', vendor: '%s') fails", logger.exception("authentication (name: '%s', vendor: '%s') fails",
self.name, self.vendor) self.name, self.vendor)
raise MoulinetteError(errno.EACCES, m18n.g('unable_authenticate')) raise MoulinetteError('unable_authenticate')
# Store session # Store session
if store_session: if store_session:
@ -138,9 +137,13 @@ class BaseAuthenticator(object):
"""Store a session and its associated password""" """Store a session and its associated password"""
gpg = gnupg.GPG() gpg = gnupg.GPG()
gpg.encoding = 'utf-8' gpg.encoding = 'utf-8'
# Encrypt the password using the session hash
s = str(gpg.encrypt(password, None, symmetric=True, passphrase=session_hash))
assert len(s), "For some reason GPG can't perform encryption, maybe check /root/.gnupg/gpg.conf or re-run with gpg = gnupg.GPG(verbose=True) ?"
with self._open_sessionfile(session_id, 'w') as f: with self._open_sessionfile(session_id, 'w') as f:
f.write(str(gpg.encrypt(password, None, symmetric=True, f.write(s)
passphrase=session_hash)))
def _retrieve_session(self, session_id, session_hash): def _retrieve_session(self, session_id, session_hash):
"""Retrieve a session and return its associated password""" """Retrieve a session and return its associated password"""
@ -149,8 +152,7 @@ class BaseAuthenticator(object):
enc_pwd = f.read() enc_pwd = f.read()
except IOError: except IOError:
logger.debug("unable to retrieve session", exc_info=1) logger.debug("unable to retrieve session", exc_info=1)
raise MoulinetteError(errno.ENOENT, raise MoulinetteError('unable_retrieve_session')
m18n.g('unable_retrieve_session'))
else: else:
gpg = gnupg.GPG() gpg = gnupg.GPG()
gpg.encoding = 'utf-8' gpg.encoding = 'utf-8'
@ -159,6 +161,5 @@ class BaseAuthenticator(object):
if decrypted.ok is not True: if decrypted.ok is not True:
logger.error("unable to decrypt password for the session: %s", logger.error("unable to decrypt password for the session: %s",
decrypted.status) decrypted.status)
raise MoulinetteError(errno.EINVAL, raise MoulinetteError('unable_retrieve_session')
m18n.g('unable_retrieve_session'))
return decrypted.data return decrypted.data

View file

@ -2,7 +2,6 @@
# TODO: Use Python3 to remove this fix! # TODO: Use Python3 to remove this fix!
from __future__ import absolute_import from __future__ import absolute_import
import errno
import logging import logging
import random import random
import string import string
@ -11,7 +10,6 @@ import ldap
import ldap.sasl import ldap.sasl
import ldap.modlist as modlist import ldap.modlist as modlist
from moulinette import m18n
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.authenticators import BaseAuthenticator from moulinette.authenticators import BaseAuthenticator
@ -21,6 +19,7 @@ logger = logging.getLogger('moulinette.authenticator.ldap')
# LDAP Class Implementation -------------------------------------------- # LDAP Class Implementation --------------------------------------------
class Authenticator(BaseAuthenticator): class Authenticator(BaseAuthenticator):
"""LDAP Authenticator """LDAP Authenticator
Initialize a LDAP connexion for the given arguments. It attempts to Initialize a LDAP connexion for the given arguments. It attempts to
@ -80,7 +79,7 @@ class Authenticator(BaseAuthenticator):
def authenticate(self, password): def authenticate(self, password):
try: try:
con = ldap.initialize(self.uri) con = ldap.ldapobject.ReconnectLDAPObject(self.uri, retry_max=10, retry_delay=0.5)
if self.userdn: if self.userdn:
if 'cn=external,cn=auth' in self.userdn: if 'cn=external,cn=auth' in self.userdn:
con.sasl_non_interactive_bind_s('EXTERNAL') con.sasl_non_interactive_bind_s('EXTERNAL')
@ -89,10 +88,10 @@ class Authenticator(BaseAuthenticator):
else: else:
con.simple_bind_s() con.simple_bind_s()
except ldap.INVALID_CREDENTIALS: except ldap.INVALID_CREDENTIALS:
raise MoulinetteError(errno.EACCES, m18n.g('invalid_password')) raise MoulinetteError('invalid_password')
except ldap.SERVER_DOWN: except ldap.SERVER_DOWN:
logger.exception('unable to reach the server to authenticate') logger.exception('unable to reach the server to authenticate')
raise MoulinetteError(169, m18n.g('ldap_server_down')) raise MoulinetteError('ldap_server_down')
else: else:
self.con = con self.con = con
self._ensure_password_uses_strong_hash(password) self._ensure_password_uses_strong_hash(password)
@ -144,7 +143,7 @@ class Authenticator(BaseAuthenticator):
except Exception as e: except Exception as e:
logger.exception("error during LDAP search operation with: base='%s', " logger.exception("error during LDAP search operation with: base='%s', "
"filter='%s', attrs=%s and exception %s", base, filter, attrs, e) "filter='%s', attrs=%s and exception %s", base, filter, attrs, e)
raise MoulinetteError(169, m18n.g('ldap_operation_error')) raise MoulinetteError('ldap_operation_error')
result_list = [] result_list = []
if not attrs or 'dn' not in attrs: if not attrs or 'dn' not in attrs:
@ -175,7 +174,7 @@ class Authenticator(BaseAuthenticator):
except Exception as e: except Exception as e:
logger.exception("error during LDAP add operation with: rdn='%s', " logger.exception("error during LDAP add operation with: rdn='%s', "
"attr_dict=%s and exception %s", rdn, attr_dict, e) "attr_dict=%s and exception %s", rdn, attr_dict, e)
raise MoulinetteError(169, m18n.g('ldap_operation_error')) raise MoulinetteError('ldap_operation_error')
else: else:
return True return True
@ -195,7 +194,7 @@ class Authenticator(BaseAuthenticator):
self.con.delete_s(dn) self.con.delete_s(dn)
except Exception as e: except Exception as e:
logger.exception("error during LDAP delete operation with: rdn='%s' and exception %s", rdn, e) logger.exception("error during LDAP delete operation with: rdn='%s' and exception %s", rdn, e)
raise MoulinetteError(169, m18n.g('ldap_operation_error')) raise MoulinetteError('ldap_operation_error')
else: else:
return True return True
@ -226,7 +225,7 @@ class Authenticator(BaseAuthenticator):
logger.exception("error during LDAP update operation with: rdn='%s', " logger.exception("error during LDAP update operation with: rdn='%s', "
"attr_dict=%s, new_rdn=%s and exception: %s", rdn, attr_dict, "attr_dict=%s, new_rdn=%s and exception: %s", rdn, attr_dict,
new_rdn, e) new_rdn, e)
raise MoulinetteError(169, m18n.g('ldap_operation_error')) raise MoulinetteError('ldap_operation_error')
else: else:
return True return True
@ -240,14 +239,30 @@ class Authenticator(BaseAuthenticator):
Returns: Returns:
Boolean | MoulinetteError Boolean | MoulinetteError
"""
attr_found = self.get_conflict(value_dict)
if attr_found:
logger.info("attribute '%s' with value '%s' is not unique",
attr_found[0], attr_found[1])
raise MoulinetteError('ldap_attribute_already_exists',
attribute=attr_found[0],
value=attr_found[1])
return True
def get_conflict(self, value_dict, base_dn=None):
"""
Check uniqueness of values
Keyword arguments:
value_dict -- Dictionnary of attributes/values to check
Returns:
None | list with Fist conflict attribute name and value
""" """
for attr, value in value_dict.items(): for attr, value in value_dict.items():
if not self.search(filter=attr + '=' + value): if not self.search(base=base_dn, filter=attr + '=' + value):
continue continue
else: else:
logger.info("attribute '%s' with value '%s' is not unique", return (attr, value)
attr, value) return None
raise MoulinetteError(errno.EEXIST,
m18n.g('ldap_attribute_already_exists',
attribute=attr, value=value))
return True

View file

@ -3,7 +3,6 @@
import os import os
import time import time
import json import json
import errno
import logging import logging
import psutil import psutil
@ -20,6 +19,7 @@ logger = logging.getLogger('moulinette.core')
# Internationalization ------------------------------------------------- # Internationalization -------------------------------------------------
class Translator(object): class Translator(object):
"""Internationalization class """Internationalization class
Provide an internationalization mechanism based on JSON files to Provide an internationalization mechanism based on JSON files to
@ -138,6 +138,7 @@ class Translator(object):
class Moulinette18n(object): class Moulinette18n(object):
"""Internationalization service for the moulinette """Internationalization service for the moulinette
Manage internationalization and access to the proper keys translation Manage internationalization and access to the proper keys translation
@ -215,6 +216,7 @@ class Moulinette18n(object):
class MoulinetteSignals(object): class MoulinetteSignals(object):
"""Signals connector for the moulinette """Signals connector for the moulinette
Allow to easily connect signals from the moulinette to handlers. A Allow to easily connect signals from the moulinette to handlers. A
@ -344,7 +346,7 @@ def init_interface(name, kwargs={}, actionsmap={}):
mod = import_module('moulinette.interfaces.%s' % name) mod = import_module('moulinette.interfaces.%s' % name)
except ImportError: except ImportError:
logger.exception("unable to load interface '%s'", name) logger.exception("unable to load interface '%s'", name)
raise MoulinetteError(errno.EINVAL, moulinette.m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
else: else:
try: try:
# Retrieve interface classes # Retrieve interface classes
@ -352,7 +354,7 @@ def init_interface(name, kwargs={}, actionsmap={}):
interface = mod.Interface interface = mod.Interface
except AttributeError: except AttributeError:
logger.exception("unable to retrieve classes of interface '%s'", name) logger.exception("unable to retrieve classes of interface '%s'", name)
raise MoulinetteError(errno.EIO, moulinette.m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
# Instantiate or retrieve ActionsMap # Instantiate or retrieve ActionsMap
if isinstance(actionsmap, dict): if isinstance(actionsmap, dict):
@ -361,12 +363,12 @@ def init_interface(name, kwargs={}, actionsmap={}):
amap = actionsmap amap = actionsmap
else: else:
logger.error("invalid actionsmap value %r", actionsmap) logger.error("invalid actionsmap value %r", actionsmap)
raise MoulinetteError(errno.EINVAL, moulinette.m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
return interface(amap, **kwargs) return interface(amap, **kwargs)
def init_authenticator((vendor, name), kwargs={}): def init_authenticator(vendor_and_name, kwargs={}):
"""Return a new authenticator instance """Return a new authenticator instance
Retrieve the given authenticator vendor and return a new instance of Retrieve the given authenticator vendor and return a new instance of
@ -378,11 +380,12 @@ def init_authenticator((vendor, name), kwargs={}):
- kwargs -- A dict of arguments for the authenticator profile - kwargs -- A dict of arguments for the authenticator profile
""" """
(vendor, name) = vendor_and_name
try: try:
mod = import_module('moulinette.authenticators.%s' % vendor) mod = import_module('moulinette.authenticators.%s' % vendor)
except ImportError: except ImportError:
logger.exception("unable to load authenticator vendor '%s'", vendor) logger.exception("unable to load authenticator vendor '%s'", vendor)
raise MoulinetteError(errno.EINVAL, moulinette.m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
else: else:
return mod.Authenticator(name, **kwargs) return mod.Authenticator(name, **kwargs)
@ -411,12 +414,21 @@ def clean_session(session_id, profiles=[]):
# Moulinette core classes ---------------------------------------------- # Moulinette core classes ----------------------------------------------
class MoulinetteError(OSError): class MoulinetteError(Exception):
"""Moulinette base exception""" """Moulinette base exception"""
pass
def __init__(self, key, raw_msg=False, *args, **kwargs):
if raw_msg:
msg = key
else:
msg = moulinette.m18n.g(key, *args, **kwargs)
super(MoulinetteError, self).__init__(msg)
self.strerror = msg
class MoulinetteLock(object): class MoulinetteLock(object):
"""Locker for a moulinette instance """Locker for a moulinette instance
It provides a lock mechanism for a given moulinette instance. It can It provides a lock mechanism for a given moulinette instance. It can
@ -471,8 +483,7 @@ class MoulinetteLock(object):
break break
if self.timeout is not None and (time.time() - start_time) > self.timeout: if self.timeout is not None and (time.time() - start_time) > self.timeout:
raise MoulinetteError(errno.EBUSY, raise MoulinetteError('instance_already_running')
moulinette.m18n.g('instance_already_running'))
# Wait before checking again # Wait before checking again
time.sleep(self.interval) time.sleep(self.interval)
@ -486,7 +497,10 @@ class MoulinetteLock(object):
""" """
if self._locked: if self._locked:
os.unlink(self._lockfile) if os.path.exists(self._lockfile):
os.unlink(self._lockfile)
else:
logger.warning("Uhoh, somehow the lock %s did not exist ..." % self._lockfile)
logger.debug('lock has been released') logger.debug('lock has been released')
self._locked = False self._locked = False
@ -495,10 +509,7 @@ class MoulinetteLock(object):
with open(self._lockfile, 'w') as f: with open(self._lockfile, 'w') as f:
f.write(str(os.getpid())) f.write(str(os.getpid()))
except IOError: except IOError:
raise MoulinetteError( raise MoulinetteError('root_required')
errno.EPERM, '%s. %s.'.format(
moulinette.m18n.g('permission_denied'),
moulinette.m18n.g('root_required')))
def _lock_PIDs(self): def _lock_PIDs(self):

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
import errno
import logging import logging
import argparse import argparse
import copy import copy
@ -20,6 +19,7 @@ CALLBACKS_PROP = '_callbacks'
# Base Class ----------------------------------------------------------- # Base Class -----------------------------------------------------------
class BaseActionsMapParser(object): class BaseActionsMapParser(object):
"""Actions map's base Parser """Actions map's base Parser
Each interfaces must implement an ActionsMapParser class derived Each interfaces must implement an ActionsMapParser class derived
@ -142,7 +142,7 @@ class BaseActionsMapParser(object):
# Validate tid and namespace # Validate tid and namespace
if not isinstance(tid, tuple) and \ if not isinstance(tid, tuple) and \
(namespace is None or not hasattr(namespace, TO_RETURN_PROP)): (namespace is None or not hasattr(namespace, TO_RETURN_PROP)):
raise MoulinetteError(errno.EINVAL, m18n.g('invalid_usage')) raise MoulinetteError('invalid_usage')
elif not tid: elif not tid:
tid = GLOBAL_SECTION tid = GLOBAL_SECTION
@ -158,8 +158,7 @@ class BaseActionsMapParser(object):
# TODO: Catch errors # TODO: Catch errors
auth = msignals.authenticate(cls(), **auth_conf) auth = msignals.authenticate(cls(), **auth_conf)
if not auth.is_authenticated: if not auth.is_authenticated:
raise MoulinetteError(errno.EACCES, raise MoulinetteError('authentication_required_long')
m18n.g('authentication_required_long'))
if self.get_conf(tid, 'argument_auth') and \ if self.get_conf(tid, 'argument_auth') and \
self.get_conf(tid, 'authenticate') == 'all': self.get_conf(tid, 'authenticate') == 'all':
namespace.auth = auth namespace.auth = auth
@ -263,7 +262,7 @@ class BaseActionsMapParser(object):
else: else:
logger.error("expecting 'all', 'False' or a list for " logger.error("expecting 'all', 'False' or a list for "
"configuration 'authenticate', got %r", ifaces) "configuration 'authenticate', got %r", ifaces)
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
# -- 'authenticator' # -- 'authenticator'
try: try:
@ -278,7 +277,7 @@ class BaseActionsMapParser(object):
except KeyError: except KeyError:
logger.error("requesting profile '%s' which is undefined in " logger.error("requesting profile '%s' which is undefined in "
"global configuration of 'authenticator'", auth) "global configuration of 'authenticator'", auth)
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
elif is_global and isinstance(auth, dict): elif is_global and isinstance(auth, dict):
if len(auth) == 0: if len(auth) == 0:
logger.warning('no profile defined in global configuration ' logger.warning('no profile defined in global configuration '
@ -301,7 +300,7 @@ class BaseActionsMapParser(object):
else: else:
logger.error("expecting a dict of profile(s) or a profile name " logger.error("expecting a dict of profile(s) or a profile name "
"for configuration 'authenticator', got %r", auth) "for configuration 'authenticator', got %r", auth)
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
# -- 'argument_auth' # -- 'argument_auth'
try: try:
@ -314,7 +313,7 @@ class BaseActionsMapParser(object):
else: else:
logger.error("expecting a boolean for configuration " logger.error("expecting a boolean for configuration "
"'argument_auth', got %r", arg_auth) "'argument_auth', got %r", arg_auth)
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
# -- 'lock' # -- 'lock'
try: try:
@ -327,7 +326,7 @@ class BaseActionsMapParser(object):
else: else:
logger.error("expecting a boolean for configuration 'lock', " logger.error("expecting a boolean for configuration 'lock', "
"got %r", lock) "got %r", lock)
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
return conf return conf
@ -354,6 +353,7 @@ class BaseActionsMapParser(object):
class BaseInterface(object): class BaseInterface(object):
"""Moulinette's base Interface """Moulinette's base Interface
Each interfaces must implement an Interface class derived from this Each interfaces must implement an Interface class derived from this
@ -426,8 +426,8 @@ class _CallbackAction(argparse.Action):
value = self.callback(namespace, values, **self.callback_kwargs) value = self.callback(namespace, values, **self.callback_kwargs)
except: except:
logger.exception("cannot get value from callback method " logger.exception("cannot get value from callback method "
"'{0}'".format(self.callback_method)) "'{0}'".format(self.callback_method))
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
else: else:
if value: if value:
if self.callback_return: if self.callback_return:
@ -437,6 +437,7 @@ class _CallbackAction(argparse.Action):
class _ExtendedSubParsersAction(argparse._SubParsersAction): class _ExtendedSubParsersAction(argparse._SubParsersAction):
"""Subparsers with extended properties for argparse """Subparsers with extended properties for argparse
It provides the following additional properties at initialization, It provides the following additional properties at initialization,

View file

@ -11,6 +11,7 @@ from gevent.queue import Queue
from geventwebsocket import WebSocketError from geventwebsocket import WebSocketError
from bottle import run, request, response, Bottle, HTTPResponse from bottle import run, request, response, Bottle, HTTPResponse
from bottle import get, post, install, abort, delete, put
from moulinette import msignals, m18n, DATA_DIR from moulinette import msignals, m18n, DATA_DIR
from moulinette.core import MoulinetteError, clean_session from moulinette.core import MoulinetteError, clean_session
@ -26,12 +27,43 @@ logger = log.getLogger('moulinette.interface.api')
# API helpers ---------------------------------------------------------- # API helpers ----------------------------------------------------------
CSRF_TYPES = set(["text/plain",
"application/x-www-form-urlencoded",
"multipart/form-data"])
def is_csrf():
"""Checks is this is a CSRF request."""
if request.method != "POST":
return False
if request.content_type is None:
return True
content_type = request.content_type.lower().split(';')[0]
if content_type not in CSRF_TYPES:
return False
return request.headers.get("X-Requested-With") is None
# Protection against CSRF
def filter_csrf(callback):
def wrapper(*args, **kwargs):
if is_csrf():
abort(403, "CSRF protection")
else:
return callback(*args, **kwargs)
return wrapper
class LogQueues(dict): class LogQueues(dict):
"""Map of session id to queue.""" """Map of session id to queue."""
pass pass
class APIQueueHandler(logging.Handler): class APIQueueHandler(logging.Handler):
""" """
A handler class which store logging records into a queue, to be used A handler class which store logging records into a queue, to be used
and retrieved from the API. and retrieved from the API.
@ -57,6 +89,7 @@ class APIQueueHandler(logging.Handler):
class _HTTPArgumentParser(object): class _HTTPArgumentParser(object):
"""Argument parser for HTTP requests """Argument parser for HTTP requests
Object for parsing HTTP requests into Python objects. It is based Object for parsing HTTP requests into Python objects. It is based
@ -159,10 +192,11 @@ class _HTTPArgumentParser(object):
def _error(self, message): def _error(self, message):
# TODO: Raise a proper exception # TODO: Raise a proper exception
raise MoulinetteError(1, message) raise MoulinetteError(message)
class _ActionsMapPlugin(object): class _ActionsMapPlugin(object):
"""Actions map Bottle Plugin """Actions map Bottle Plugin
Process relevant action for the request using the actions map and Process relevant action for the request using the actions map and
@ -317,7 +351,7 @@ class _ActionsMapPlugin(object):
self.logout(profile) self.logout(profile)
except: except:
pass pass
raise error_to_response(e) raise HTTPUnauthorizedResponse(e.strerror)
else: else:
# Update dicts with new values # Update dicts with new values
s_hashes[profile] = s_hash s_hashes[profile] = s_hash
@ -404,7 +438,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 error_to_response(e) raise HTTPBadRequestResponse(e.strerror)
except Exception as e: except Exception as e:
if isinstance(e, HTTPResponse): if isinstance(e, HTTPResponse):
raise e raise e
@ -488,38 +522,12 @@ class HTTPUnauthorizedResponse(HTTPResponse):
super(HTTPUnauthorizedResponse, self).__init__(output, 401) super(HTTPUnauthorizedResponse, self).__init__(output, 401)
class HTTPForbiddenResponse(HTTPResponse):
def __init__(self, output=''):
super(HTTPForbiddenResponse, self).__init__(output, 403)
class HTTPErrorResponse(HTTPResponse): class HTTPErrorResponse(HTTPResponse):
def __init__(self, output=''): def __init__(self, output=''):
super(HTTPErrorResponse, self).__init__(output, 500) super(HTTPErrorResponse, self).__init__(output, 500)
def error_to_response(error):
"""Convert a MoulinetteError to relevant HTTP response."""
if error.errno == errno.EPERM:
return HTTPForbiddenResponse(error.strerror)
elif error.errno == errno.EACCES:
return HTTPUnauthorizedResponse(error.strerror)
# Client-side error
elif error.errno in [errno.ENOENT, errno.ESRCH, errno.ENXIO, errno.EEXIST,
errno.ENODEV, errno.EINVAL, errno.ENOPKG, errno.EDESTADDRREQ]:
return HTTPBadRequestResponse(error.strerror)
# Server-side error
elif error.errno in [errno.EIO, errno.EBUSY, errno.ENODATA, errno.EINTR,
errno.ENETUNREACH]:
return HTTPErrorResponse(error.strerror)
else:
logger.debug('unknown relevant response for error [%s] %s',
error.errno, error.strerror)
return HTTPErrorResponse(error.strerror)
def format_for_response(content): def format_for_response(content):
"""Format the resulted content of a request for the HTTP response.""" """Format the resulted content of a request for the HTTP response."""
if request.method == 'POST': if request.method == 'POST':
@ -541,6 +549,7 @@ def format_for_response(content):
# API Classes Implementation ------------------------------------------- # API Classes Implementation -------------------------------------------
class ActionsMapParser(BaseActionsMapParser): class ActionsMapParser(BaseActionsMapParser):
"""Actions map's Parser for the API """Actions map's Parser for the API
Provide actions map parsing methods for a CLI usage. The parser for Provide actions map parsing methods for a CLI usage. The parser for
@ -630,7 +639,7 @@ class ActionsMapParser(BaseActionsMapParser):
tid, parser = self._parsers[route] tid, parser = self._parsers[route]
except KeyError: except KeyError:
logger.error("no argument parser found for route '%s'", route) logger.error("no argument parser found for route '%s'", route)
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
ret = argparse.Namespace() ret = argparse.Namespace()
# Perform authentication if needed # Perform authentication if needed
@ -643,7 +652,7 @@ class ActionsMapParser(BaseActionsMapParser):
# TODO: Catch errors # TODO: Catch errors
auth = msignals.authenticate(klass(), **auth_conf) auth = msignals.authenticate(klass(), **auth_conf)
if not auth.is_authenticated: if not auth.is_authenticated:
raise MoulinetteError(errno.EACCES, m18n.g('authentication_required_long')) raise MoulinetteError('authentication_required_long')
if self.get_conf(tid, 'argument_auth') and \ if self.get_conf(tid, 'argument_auth') and \
self.get_conf(tid, 'authenticate') == 'all': self.get_conf(tid, 'authenticate') == 'all':
ret.auth = auth ret.auth = auth
@ -677,6 +686,7 @@ class ActionsMapParser(BaseActionsMapParser):
class Interface(BaseInterface): class Interface(BaseInterface):
"""Application Programming Interface for the moulinette """Application Programming Interface for the moulinette
Initialize a HTTP server which serves the API connected to a given Initialize a HTTP server which serves the API connected to a given
@ -722,6 +732,7 @@ class Interface(BaseInterface):
return callback return callback
# Install plugins # Install plugins
app.install(filter_csrf)
app.install(apiheader) app.install(apiheader)
app.install(api18n) app.install(api18n)
app.install(_ActionsMapPlugin(actionsmap, use_websocket, log_queues)) app.install(_ActionsMapPlugin(actionsmap, use_websocket, log_queues))
@ -765,9 +776,8 @@ class Interface(BaseInterface):
logger.exception("unable to start the server instance on %s:%d", logger.exception("unable to start the server instance on %s:%d",
host, port) host, port)
if e.args[0] == errno.EADDRINUSE: if e.args[0] == errno.EADDRINUSE:
raise MoulinetteError(errno.EADDRINUSE, raise MoulinetteError('server_already_running')
m18n.g('server_already_running')) raise MoulinetteError('error_see_log')
raise MoulinetteError(errno.EIO, m18n.g('error_see_log'))
# Routes handlers # Routes handlers

View file

@ -2,12 +2,14 @@
import os import os
import sys import sys
import errno
import getpass import getpass
import locale import locale
import logging import logging
from argparse import SUPPRESS from argparse import SUPPRESS
from collections import OrderedDict from collections import OrderedDict
import time
import pytz
from datetime import date, datetime
import argcomplete import argcomplete
@ -94,6 +96,32 @@ def plain_print_dict(d, depth=0):
print(d) print(d)
def pretty_date(_date):
"""Display a date in the current time zone without ms and tzinfo
Argument:
- date -- The date or datetime to display
"""
# Deduce system timezone
nowutc = datetime.now(tz=pytz.utc)
nowtz = datetime.now()
nowtz = nowtz.replace(tzinfo=pytz.utc)
offsetHour = nowutc - nowtz
offsetHour = int(round(offsetHour.total_seconds() / 3600))
localtz = 'Etc/GMT%+d' % offsetHour
# Transform naive date into UTC date
if _date.tzinfo is None:
_date = _date.replace(tzinfo=pytz.utc)
# Convert UTC date into system locale date
_date = _date.astimezone(pytz.timezone(localtz))
if isinstance(_date, datetime):
return _date.strftime("%Y-%m-%d %H:%M:%S")
else:
return _date.strftime("%Y-%m-%d")
def pretty_print_dict(d, depth=0): def pretty_print_dict(d, depth=0):
"""Print in a pretty way a dictionary recursively """Print in a pretty way a dictionary recursively
@ -127,10 +155,14 @@ def pretty_print_dict(d, depth=0):
else: else:
if isinstance(value, unicode): if isinstance(value, unicode):
value = value.encode('utf-8') value = value.encode('utf-8')
elif isinstance(v, date):
v = pretty_date(v)
print("{:s}- {}".format(" " * (depth + 1), value)) print("{:s}- {}".format(" " * (depth + 1), value))
else: else:
if isinstance(v, unicode): if isinstance(v, unicode):
v = v.encode('utf-8') v = v.encode('utf-8')
elif isinstance(v, date):
v = pretty_date(v)
print("{:s}{}: {}".format(" " * depth, k, v)) print("{:s}{}: {}".format(" " * depth, k, v))
@ -145,6 +177,7 @@ def get_locale():
# CLI Classes Implementation ------------------------------------------- # CLI Classes Implementation -------------------------------------------
class TTYHandler(logging.StreamHandler): class TTYHandler(logging.StreamHandler):
"""TTY log handler """TTY log handler
A handler class which prints logging records for a tty. The record is A handler class which prints logging records for a tty. The record is
@ -210,6 +243,7 @@ class TTYHandler(logging.StreamHandler):
class ActionsMapParser(BaseActionsMapParser): class ActionsMapParser(BaseActionsMapParser):
"""Actions map's Parser for the CLI """Actions map's Parser for the CLI
Provide actions map parsing methods for a CLI usage. The parser for Provide actions map parsing methods for a CLI usage. The parser for
@ -329,7 +363,7 @@ class ActionsMapParser(BaseActionsMapParser):
raise raise
except: except:
logger.exception("unable to parse arguments '%s'", ' '.join(args)) logger.exception("unable to parse arguments '%s'", ' '.join(args))
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) raise MoulinetteError('error_see_log')
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)
@ -337,6 +371,7 @@ class ActionsMapParser(BaseActionsMapParser):
class Interface(BaseInterface): class Interface(BaseInterface):
"""Command-line Interface for the moulinette """Command-line Interface for the moulinette
Initialize an interface connected to the standard input/output Initialize an interface connected to the standard input/output
@ -376,7 +411,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(errno.EINVAL, m18n.g('invalid_usage')) raise MoulinetteError('invalid_usage')
# auto-complete # auto-complete
argcomplete.autocomplete(self.actionsmap.parser._parser) argcomplete.autocomplete(self.actionsmap.parser._parser)
@ -389,7 +424,7 @@ class Interface(BaseInterface):
try: try:
ret = self.actionsmap.process(args, timeout=timeout) ret = self.actionsmap.process(args, timeout=timeout)
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
raise MoulinetteError(errno.EINTR, m18n.g('operation_interrupted')) raise MoulinetteError('operation_interrupted')
if ret is None or output_as == 'none': if ret is None or output_as == 'none':
return return
@ -439,7 +474,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(errno.EINVAL, m18n.g('values_mismatch')) raise MoulinetteError('values_mismatch')
return value return value

View file

@ -23,21 +23,16 @@ def read_file(file_path):
# Check file exists # Check file exists
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
raise MoulinetteError(errno.ENOENT, raise MoulinetteError('file_not_exist', path=file_path)
m18n.g('file_not_exist', path=file_path))
# Open file and read content # Open file and read content
try: try:
with open(file_path, "r") as f: with open(file_path, "r") as f:
file_content = f.read() file_content = f.read()
except IOError as e: except IOError as e:
raise MoulinetteError(errno.EACCES, raise MoulinetteError('cannot_open_file', file=file_path, error=str(e))
m18n.g('cannot_open_file',
file=file_path, error=str(e)))
except Exception as e: except Exception as e:
raise MoulinetteError(errno.EIO, raise MoulinetteError('error_reading_file', file=file_path, error=str(e))
m18n.g('error_reading_file',
file=file_path, error=str(e)))
return file_content return file_content
@ -57,9 +52,7 @@ def read_json(file_path):
try: try:
loaded_json = json.loads(file_content) loaded_json = json.loads(file_content)
except ValueError as e: except ValueError as e:
raise MoulinetteError(errno.EINVAL, raise MoulinetteError('corrupted_json', ressource=file_path, error=str(e))
m18n.g('corrupted_json',
ressource=file_path, error=str(e)))
return loaded_json return loaded_json
@ -79,9 +72,7 @@ def read_yaml(file_path):
try: try:
loaded_yaml = yaml.safe_load(file_content) loaded_yaml = yaml.safe_load(file_content)
except ValueError as e: except ValueError as e:
raise MoulinetteError(errno.EINVAL, raise MoulinetteError('corrupted_yaml', ressource=file_path, error=str(e))
m18n.g('corrupted_yaml',
ressource=file_path, error=str(e)))
return loaded_yaml return loaded_yaml
@ -111,13 +102,9 @@ def write_to_file(file_path, data, file_mode="w"):
with open(file_path, file_mode) as f: with open(file_path, file_mode) as f:
f.write(data) f.write(data)
except IOError as e: except IOError as e:
raise MoulinetteError(errno.EACCES, raise MoulinetteError('cannot_write_file', file=file_path, error=str(e))
m18n.g('cannot_write_file',
file=file_path, error=str(e)))
except Exception as e: except Exception as e:
raise MoulinetteError(errno.EIO, raise MoulinetteError('error_writing_file', file=file_path, error=str(e))
m18n.g('error_writing_file',
file=file_path, error=str(e)))
def append_to_file(file_path, data): def append_to_file(file_path, data):
@ -152,16 +139,12 @@ def write_to_json(file_path, data):
with open(file_path, "w") as f: with open(file_path, "w") as f:
json.dump(data, f) json.dump(data, f)
except IOError as e: except IOError as e:
raise MoulinetteError(errno.EACCES, raise MoulinetteError('cannot_write_file', file=file_path, error=str(e))
m18n.g('cannot_write_file',
file=file_path, error=str(e)))
except Exception as e: except Exception as e:
raise MoulinetteError(errno.EIO, raise MoulinetteError('_error_writing_file', file=file_path, error=str(e))
m18n.g('_error_writing_file',
file=file_path, error=str(e)))
def mkdir(path, mode=0777, parents=False, uid=None, gid=None, force=False): def mkdir(path, mode=0o777, parents=False, uid=None, gid=None, force=False):
"""Create a directory with optional features """Create a directory with optional features
Create a directory and optionaly set its permissions to mode and its Create a directory and optionaly set its permissions to mode and its
@ -223,16 +206,14 @@ def chown(path, uid=None, gid=None, recursive=False):
try: try:
uid = getpwnam(uid).pw_uid uid = getpwnam(uid).pw_uid
except KeyError: except KeyError:
raise MoulinetteError(errno.EINVAL, raise MoulinetteError('unknown_user', user=uid)
m18n.g('unknown_user', user=uid))
elif uid is None: elif uid is None:
uid = -1 uid = -1
if isinstance(gid, basestring): if isinstance(gid, basestring):
try: try:
gid = grp.getgrnam(gid).gr_gid gid = grp.getgrnam(gid).gr_gid
except KeyError: except KeyError:
raise MoulinetteError(errno.EINVAL, raise MoulinetteError('unknown_group', group=gid)
m18n.g('unknown_group', group=gid))
elif gid is None: elif gid is None:
gid = -1 gid = -1
@ -245,9 +226,7 @@ def chown(path, uid=None, gid=None, recursive=False):
for f in files: for f in files:
os.chown(os.path.join(root, f), uid, gid) os.chown(os.path.join(root, f), uid, gid)
except Exception as e: except Exception as e:
raise MoulinetteError(errno.EIO, raise MoulinetteError('error_changing_file_permissions', path=path, error=str(e))
m18n.g('error_changing_file_permissions',
path=path, error=str(e)))
def chmod(path, mode, fmode=None, recursive=False): def chmod(path, mode, fmode=None, recursive=False):
@ -271,9 +250,7 @@ def chmod(path, mode, fmode=None, recursive=False):
for f in files: for f in files:
os.chmod(os.path.join(root, f), fmode) os.chmod(os.path.join(root, f), fmode)
except Exception as e: except Exception as e:
raise MoulinetteError(errno.EIO, raise MoulinetteError('error_changing_file_permissions', path=path, error=str(e))
m18n.g('error_changing_file_permissions',
path=path, error=str(e)))
def rm(path, recursive=False, force=False): def rm(path, recursive=False, force=False):
@ -292,6 +269,4 @@ def rm(path, recursive=False, force=False):
os.remove(path) os.remove(path)
except OSError as e: except OSError as e:
if not force: if not force:
raise MoulinetteError(errno.EIO, raise MoulinetteError('error_removing', path=path, error=str(e))
m18n.g('error_removing',
path=path, error=str(e)))

View file

@ -70,6 +70,7 @@ def getHandlersByClass(classinfo, limit=0):
class MoulinetteLogger(Logger): class MoulinetteLogger(Logger):
"""Custom logger class """Custom logger class
Extend base Logger class to provide the SUCCESS custom log level with Extend base Logger class to provide the SUCCESS custom log level with
@ -153,6 +154,7 @@ def getActionLogger(name=None, logger=None, action_id=None):
class ActionFilter(object): class ActionFilter(object):
"""Extend log record for an optionnal action """Extend log record for an optionnal action
Filter a given record and look for an `action_id` key. If it is not found Filter a given record and look for an `action_id` key. If it is not found

View file

@ -1,7 +1,5 @@
import errno
import json import json
from moulinette import m18n
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
@ -25,27 +23,22 @@ def download_text(url, timeout=30, expected_status_code=200):
r = requests.get(url, timeout=timeout) r = requests.get(url, timeout=timeout)
# Invalid URL # Invalid URL
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
raise MoulinetteError(errno.EBADE, raise MoulinetteError('invalid_url', url=url)
m18n.g('invalid_url', url=url))
# SSL exceptions # SSL exceptions
except requests.exceptions.SSLError: except requests.exceptions.SSLError:
raise MoulinetteError(errno.EBADE, raise MoulinetteError('download_ssl_error', url=url)
m18n.g('download_ssl_error', url=url))
# Timeout exceptions # Timeout exceptions
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
raise MoulinetteError(errno.ETIME, raise MoulinetteError('download_timeout', url=url)
m18n.g('download_timeout', url=url))
# Unknown stuff # Unknown stuff
except Exception as e: except Exception as e:
raise MoulinetteError(errno.ECONNRESET, raise MoulinetteError('download_unknown_error',
m18n.g('download_unknown_error', url=url, error=str(e))
url=url, error=str(e)))
# Assume error if status code is not 200 (OK) # Assume error if status code is not 200 (OK)
if expected_status_code is not None \ if expected_status_code is not None \
and r.status_code != expected_status_code: and r.status_code != expected_status_code:
raise MoulinetteError(errno.EBADE, raise MoulinetteError('download_bad_status_code',
m18n.g('download_bad_status_code', url=url, code=str(r.status_code))
url=url, code=str(r.status_code)))
return r.text return r.text
@ -66,7 +59,6 @@ def download_json(url, timeout=30, expected_status_code=200):
try: try:
loaded_json = json.loads(text) loaded_json = json.loads(text)
except ValueError: except ValueError:
raise MoulinetteError(errno.EINVAL, raise MoulinetteError('corrupted_json', ressource=url)
m18n.g('corrupted_json', ressource=url))
return loaded_json return loaded_json

View file

@ -60,12 +60,13 @@ def call_async_output(args, callback, **kwargs):
if "stdinfo" in kwargs and kwargs["stdinfo"] is not None: if "stdinfo" in kwargs and kwargs["stdinfo"] is not None:
assert len(callback) == 3 assert len(callback) == 3
stdinfo = kwargs.pop("stdinfo") stdinfo = kwargs.pop("stdinfo")
os.mkfifo(stdinfo, 0600) os.mkfifo(stdinfo, 0o600)
# Open stdinfo for reading (in a nonblocking way, i.e. even # Open stdinfo for reading (in a nonblocking way, i.e. even
# if command does not write in the stdinfo pipe...) # if command does not write in the stdinfo pipe...)
stdinfo_f = os.open(stdinfo, os.O_RDONLY | os.O_NONBLOCK) stdinfo_f = os.open(stdinfo, os.O_RDONLY | os.O_NONBLOCK)
else: else:
kwargs.pop("stdinfo") if "stdinfo" in kwargs:
kwargs.pop("stdinfo")
stdinfo = None stdinfo = None
# Validate callback argument # Validate callback argument
@ -98,13 +99,15 @@ def call_async_output(args, callback, **kwargs):
# this way is not 100% perfect but should do it # this way is not 100% perfect but should do it
stdout_consum.process_next_line() stdout_consum.process_next_line()
stderr_consum.process_next_line() stderr_consum.process_next_line()
stdinfo_consum.process_next_line() if stdinfo:
stdinfo_consum.process_next_line()
time.sleep(.1) time.sleep(.1)
stderr_reader.join() stderr_reader.join()
# clear the queues # clear the queues
stdout_consum.process_current_queue() stdout_consum.process_current_queue()
stderr_consum.process_current_queue() stderr_consum.process_current_queue()
stdinfo_consum.process_current_queue() if stdinfo:
stdinfo_consum.process_current_queue()
else: else:
while not stdout_reader.eof(): while not stdout_reader.eof():
stdout_consum.process_current_queue() stdout_consum.process_current_queue()

View file

@ -1,6 +1,7 @@
import logging import logging
from json.encoder import JSONEncoder from json.encoder import JSONEncoder
import datetime import datetime
import pytz
logger = logging.getLogger('moulinette.utils.serialize') logger = logging.getLogger('moulinette.utils.serialize')
@ -8,6 +9,7 @@ logger = logging.getLogger('moulinette.utils.serialize')
# JSON utilities ------------------------------------------------------- # JSON utilities -------------------------------------------------------
class JSONExtendedEncoder(JSONEncoder): class JSONExtendedEncoder(JSONEncoder):
"""Extended JSON encoder """Extended JSON encoder
Extend default JSON encoder to recognize more types and classes. It Extend default JSON encoder to recognize more types and classes. It
@ -26,7 +28,9 @@ class JSONExtendedEncoder(JSONEncoder):
return list(o) return list(o)
# Display the date in its iso format ISO-8601 Internet Profile (RFC 3339) # Display the date in its iso format ISO-8601 Internet Profile (RFC 3339)
if isinstance(o, datetime.datetime) or isinstance(o, datetime.date): if isinstance(o, datetime.date):
if o.tzinfo is None:
o = o.replace(tzinfo=pytz.utc)
return o.isoformat() return o.isoformat()
# Return the repr for object that json can't encode # Return the repr for object that json can't encode

View file

@ -8,6 +8,7 @@ from multiprocessing.queues import SimpleQueue
# Read from a stream --------------------------------------------------- # Read from a stream ---------------------------------------------------
class AsynchronousFileReader(Process): class AsynchronousFileReader(Process):
""" """
Helper class to implement asynchronous reading of a file Helper class to implement asynchronous reading of a file
in a separate thread. Pushes read lines on a queue to in a separate thread. Pushes read lines on a queue to
@ -74,6 +75,7 @@ class AsynchronousFileReader(Process):
class Consummer(object): class Consummer(object):
def __init__(self, queue, callback): def __init__(self, queue, callback):
self.queue = queue self.queue = queue
self.callback = callback self.callback = callback

View file

@ -30,5 +30,6 @@ setup(name='Moulinette',
'moulinette.interfaces', 'moulinette.interfaces',
'moulinette.utils', 'moulinette.utils',
], ],
data_files=[(LOCALES_DIR, locale_files)] data_files=[(LOCALES_DIR, locale_files)],
tests_require=["pytest", "webtest"],
) )

100
tests/test_api.py Normal file
View file

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
from webtest import TestApp as WebTestApp
from bottle import Bottle
from moulinette.interfaces.api import filter_csrf
URLENCODED = 'application/x-www-form-urlencoded'
FORMDATA = 'multipart/form-data'
TEXT = 'text/plain'
TYPES = [URLENCODED, FORMDATA, TEXT]
SAFE_METHODS = ["HEAD", "GET", "PUT", "DELETE"]
app = Bottle(autojson=True)
app.install(filter_csrf)
@app.get('/')
def get_hello():
return "Hello World!\n"
@app.post('/')
def post_hello():
return "OK\n"
@app.put('/')
def put_hello():
return "OK\n"
@app.delete('/')
def delete_hello():
return "OK\n"
webtest = WebTestApp(app)
def test_get():
r = webtest.get("/")
assert r.status_code == 200
def test_csrf_post():
r = webtest.post("/", "test", expect_errors=True)
assert r.status_code == 403
def test_post_json():
r = webtest.post("/", "test",
headers=[("Content-Type", "application/json")])
assert r.status_code == 200
def test_csrf_post_text():
r = webtest.post("/", "test",
headers=[("Content-Type", "text/plain")],
expect_errors=True)
assert r.status_code == 403
def test_csrf_post_urlencoded():
r = webtest.post("/", "test",
headers=[("Content-Type",
"application/x-www-form-urlencoded")],
expect_errors=True)
assert r.status_code == 403
def test_csrf_post_form():
r = webtest.post("/", "test",
headers=[("Content-Type", "multipart/form-data")],
expect_errors=True)
assert r.status_code == 403
def test_ok_post_text():
r = webtest.post("/", "test",
headers=[("Content-Type", "text/plain"),
("X-Requested-With", "XMLHttpRequest")])
assert r.status_code == 200
def test_ok_post_urlencoded():
r = webtest.post("/", "test",
headers=[("Content-Type",
"application/x-www-form-urlencoded"),
("X-Requested-With", "XMLHttpRequest")])
assert r.status_code == 200
def test_ok_post_form():
r = webtest.post("/", "test",
headers=[("Content-Type", "multipart/form-data"),
("X-Requested-With", "XMLHttpRequest")])
assert r.status_code == 200