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
* [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-gevent-websocket,
python-argcomplete,
python-psutil
python-psutil,
python-tz
Replaces: yunohost-cli
Breaks: yunohost-cli
Description: prototype interfaces with ease in Python

View file

@ -45,10 +45,11 @@
"error_removing": "خطأ أثناء عملية حذف {path:s}: {error:s}",
"error_changing_file_permissions": "خطأ أثناء عملية تعديل التصريحات لـ {path:s}: {error:s}",
"invalid_url": "خطأ في عنوان الرابط {url:s} (هل هذا الموقع موجود حقًا ؟)",
"download_ssl_error": "خطأ في الإتصال الآمن عبر الـ SSL أثناء محاولة الإتصال بـ {url:s}",
"download_ssl_error": "خطأ في الاتصال الآمن عبر الـ SSL أثناء محاولة الربط بـ {url:s}",
"download_timeout": "{url:s} استغرق مدة طويلة جدا للإستجابة، فتوقّف.",
"download_unknown_error": "خطأ أثناء عملية تنزيل البيانات مِن {url:s} : {error:s}",
"download_bad_status_code": "{url:s} أعاد رمز الحالة {code: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_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}",
"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",
"password": "Password",
"pattern_not_match": "Does not match pattern",
"permission_denied": "Permission denied",
"root_required": "You must be root to perform this action",
"server_already_running": "A server is already running on that port",
"success": "Success!",
@ -47,10 +46,10 @@
"error_writing_file": "Error when writing file {file: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}",
"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_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}",
"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_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}",
"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",
"authentication_profile_required": "Lauthentification au profil « {profile} » requise",
"argument_required": "Largument '{argument}' est requis",
"authentication_profile_required": "Lauthentification au profil '{profile}' est requise",
"authentication_required": "Authentification requise",
"authentication_required_long": "Lauthentification est requise pour exécuter cette action",
"colon": "{} : ",
"confirm": "Confirmez : {prompt}",
"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": "'{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",
"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/.",
"file_exists": "Le fichier existe déjà : « {path} »",
"file_not_exist": "Le fichier « {path} » nexiste pas",
"folder_exists": "Le dossier existe déjà : « {path} »",
"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_not_exist": "Le fichier '{path}' nexiste pas",
"folder_exists": "Le dossier existe déjà : '{path}'",
"folder_not_exist": "Le dossier nexiste pas",
"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_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_server_down": "Impossible datteindre le serveur LDAP",
"logged_in": "Connecté",
@ -32,23 +32,24 @@
"success": "Succès !",
"unable_authenticate": "Impossible de vous authentifier",
"unable_retrieve_session": "Impossible de récupérer la session",
"unknown_group": "Groupe « {group} » inconnu",
"unknown_user": "Utilisateur « {user} » inconnu",
"unknown_group": "Groupe '{group}' inconnu",
"unknown_user": "L'utilisateur « {user} » est inconnu",
"values_mismatch": "Les valeurs ne correspondent pas",
"warning": "Attention :",
"websocket_request_expected": "Requête WebSocket attendue",
"cannot_open_file": "Impossible douvrir le fichier {file:s} (cause : {error:s})",
"cannot_write_file": "Ne peut pas écrire le fichier {file:s} (cause : {error:s})",
"websocket_request_expected": "Une requête WebSocket est attendue",
"cannot_open_file": "Impossible douvrir le fichier {file:s} (raison : {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}",
"corrupted_json": "Json corrompu lu depuis {ressource:s} (cause : {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_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 ?)",
"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_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}",
"invalid_url": "URL {url:s} invalide : ce site existe-t-il ?",
"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_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}",
"command_unknown": "Commande « {command:s} » inconnue ?",
"corrupted_yaml": "YAML corrompu lu {ressource:s} depuis (cause : {error:s})"
"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_bad_status_code": "{url:s} renvoie le code d'état {code:s}",
"command_unknown": "Commande '{command:s}' inconnue ?",
"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_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}",
"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}",
"command_unknown": "Comanda {command:s} desconeguda?",
"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:
import logging
logging.getLogger(namespaces[0]).error(e.strerror)
return e.errno
return 1
return 0

View file

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

View file

@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
import errno
import gnupg
import logging
from moulinette import m18n
from moulinette.cache import open_cachefile
from moulinette.core import MoulinetteError
@ -14,6 +12,7 @@ logger = logging.getLogger('moulinette.authenticator')
# Base Class -----------------------------------------------------------
class BaseAuthenticator(object):
"""Authenticator base representation
Each authenticators must implement an Authenticator class derived
@ -97,7 +96,7 @@ class BaseAuthenticator(object):
except TypeError:
logger.error("unable to extract token parts from '%s'", token)
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")
store_session = False
@ -114,7 +113,7 @@ class BaseAuthenticator(object):
except:
logger.exception("authentication (name: '%s', vendor: '%s') fails",
self.name, self.vendor)
raise MoulinetteError(errno.EACCES, m18n.g('unable_authenticate'))
raise MoulinetteError('unable_authenticate')
# Store session
if store_session:
@ -138,9 +137,13 @@ class BaseAuthenticator(object):
"""Store a session and its associated password"""
gpg = gnupg.GPG()
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:
f.write(str(gpg.encrypt(password, None, symmetric=True,
passphrase=session_hash)))
f.write(s)
def _retrieve_session(self, session_id, session_hash):
"""Retrieve a session and return its associated password"""
@ -149,8 +152,7 @@ class BaseAuthenticator(object):
enc_pwd = f.read()
except IOError:
logger.debug("unable to retrieve session", exc_info=1)
raise MoulinetteError(errno.ENOENT,
m18n.g('unable_retrieve_session'))
raise MoulinetteError('unable_retrieve_session')
else:
gpg = gnupg.GPG()
gpg.encoding = 'utf-8'
@ -159,6 +161,5 @@ class BaseAuthenticator(object):
if decrypted.ok is not True:
logger.error("unable to decrypt password for the session: %s",
decrypted.status)
raise MoulinetteError(errno.EINVAL,
m18n.g('unable_retrieve_session'))
raise MoulinetteError('unable_retrieve_session')
return decrypted.data

View file

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

View file

@ -3,7 +3,6 @@
import os
import time
import json
import errno
import logging
import psutil
@ -20,6 +19,7 @@ logger = logging.getLogger('moulinette.core')
# Internationalization -------------------------------------------------
class Translator(object):
"""Internationalization class
Provide an internationalization mechanism based on JSON files to
@ -138,6 +138,7 @@ class Translator(object):
class Moulinette18n(object):
"""Internationalization service for the moulinette
Manage internationalization and access to the proper keys translation
@ -215,6 +216,7 @@ class Moulinette18n(object):
class MoulinetteSignals(object):
"""Signals connector for the moulinette
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)
except ImportError:
logger.exception("unable to load interface '%s'", name)
raise MoulinetteError(errno.EINVAL, moulinette.m18n.g('error_see_log'))
raise MoulinetteError('error_see_log')
else:
try:
# Retrieve interface classes
@ -352,7 +354,7 @@ def init_interface(name, kwargs={}, actionsmap={}):
interface = mod.Interface
except AttributeError:
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
if isinstance(actionsmap, dict):
@ -361,12 +363,12 @@ def init_interface(name, kwargs={}, actionsmap={}):
amap = actionsmap
else:
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)
def init_authenticator((vendor, name), kwargs={}):
def init_authenticator(vendor_and_name, kwargs={}):
"""Return a new authenticator instance
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
"""
(vendor, name) = vendor_and_name
try:
mod = import_module('moulinette.authenticators.%s' % vendor)
except ImportError:
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:
return mod.Authenticator(name, **kwargs)
@ -411,12 +414,21 @@ def clean_session(session_id, profiles=[]):
# Moulinette core classes ----------------------------------------------
class MoulinetteError(OSError):
class MoulinetteError(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):
"""Locker for a moulinette instance
It provides a lock mechanism for a given moulinette instance. It can
@ -471,8 +483,7 @@ class MoulinetteLock(object):
break
if self.timeout is not None and (time.time() - start_time) > self.timeout:
raise MoulinetteError(errno.EBUSY,
moulinette.m18n.g('instance_already_running'))
raise MoulinetteError('instance_already_running')
# Wait before checking again
time.sleep(self.interval)
@ -486,7 +497,10 @@ class MoulinetteLock(object):
"""
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')
self._locked = False
@ -495,10 +509,7 @@ class MoulinetteLock(object):
with open(self._lockfile, 'w') as f:
f.write(str(os.getpid()))
except IOError:
raise MoulinetteError(
errno.EPERM, '%s. %s.'.format(
moulinette.m18n.g('permission_denied'),
moulinette.m18n.g('root_required')))
raise MoulinetteError('root_required')
def _lock_PIDs(self):

View file

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

View file

@ -11,6 +11,7 @@ from gevent.queue import Queue
from geventwebsocket import WebSocketError
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.core import MoulinetteError, clean_session
@ -26,12 +27,43 @@ logger = log.getLogger('moulinette.interface.api')
# 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):
"""Map of session id to queue."""
pass
class APIQueueHandler(logging.Handler):
"""
A handler class which store logging records into a queue, to be used
and retrieved from the API.
@ -57,6 +89,7 @@ class APIQueueHandler(logging.Handler):
class _HTTPArgumentParser(object):
"""Argument parser for HTTP requests
Object for parsing HTTP requests into Python objects. It is based
@ -159,10 +192,11 @@ class _HTTPArgumentParser(object):
def _error(self, message):
# TODO: Raise a proper exception
raise MoulinetteError(1, message)
raise MoulinetteError(message)
class _ActionsMapPlugin(object):
"""Actions map Bottle Plugin
Process relevant action for the request using the actions map and
@ -317,7 +351,7 @@ class _ActionsMapPlugin(object):
self.logout(profile)
except:
pass
raise error_to_response(e)
raise HTTPUnauthorizedResponse(e.strerror)
else:
# Update dicts with new values
s_hashes[profile] = s_hash
@ -404,7 +438,7 @@ class _ActionsMapPlugin(object):
try:
ret = self.actionsmap.process(arguments, timeout=30, route=_route)
except MoulinetteError as e:
raise error_to_response(e)
raise HTTPBadRequestResponse(e.strerror)
except Exception as e:
if isinstance(e, HTTPResponse):
raise e
@ -488,38 +522,12 @@ class HTTPUnauthorizedResponse(HTTPResponse):
super(HTTPUnauthorizedResponse, self).__init__(output, 401)
class HTTPForbiddenResponse(HTTPResponse):
def __init__(self, output=''):
super(HTTPForbiddenResponse, self).__init__(output, 403)
class HTTPErrorResponse(HTTPResponse):
def __init__(self, output=''):
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):
"""Format the resulted content of a request for the HTTP response."""
if request.method == 'POST':
@ -541,6 +549,7 @@ def format_for_response(content):
# API Classes Implementation -------------------------------------------
class ActionsMapParser(BaseActionsMapParser):
"""Actions map's Parser for the API
Provide actions map parsing methods for a CLI usage. The parser for
@ -630,7 +639,7 @@ class ActionsMapParser(BaseActionsMapParser):
tid, parser = self._parsers[route]
except KeyError:
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()
# Perform authentication if needed
@ -643,7 +652,7 @@ class ActionsMapParser(BaseActionsMapParser):
# TODO: Catch errors
auth = msignals.authenticate(klass(), **auth_conf)
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 \
self.get_conf(tid, 'authenticate') == 'all':
ret.auth = auth
@ -677,6 +686,7 @@ class ActionsMapParser(BaseActionsMapParser):
class Interface(BaseInterface):
"""Application Programming Interface for the moulinette
Initialize a HTTP server which serves the API connected to a given
@ -722,6 +732,7 @@ class Interface(BaseInterface):
return callback
# Install plugins
app.install(filter_csrf)
app.install(apiheader)
app.install(api18n)
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",
host, port)
if e.args[0] == errno.EADDRINUSE:
raise MoulinetteError(errno.EADDRINUSE,
m18n.g('server_already_running'))
raise MoulinetteError(errno.EIO, m18n.g('error_see_log'))
raise MoulinetteError('server_already_running')
raise MoulinetteError('error_see_log')
# Routes handlers

View file

@ -2,12 +2,14 @@
import os
import sys
import errno
import getpass
import locale
import logging
from argparse import SUPPRESS
from collections import OrderedDict
import time
import pytz
from datetime import date, datetime
import argcomplete
@ -94,6 +96,32 @@ def plain_print_dict(d, depth=0):
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):
"""Print in a pretty way a dictionary recursively
@ -127,10 +155,14 @@ def pretty_print_dict(d, depth=0):
else:
if isinstance(value, unicode):
value = value.encode('utf-8')
elif isinstance(v, date):
v = pretty_date(v)
print("{:s}- {}".format(" " * (depth + 1), value))
else:
if isinstance(v, unicode):
v = v.encode('utf-8')
elif isinstance(v, date):
v = pretty_date(v)
print("{:s}{}: {}".format(" " * depth, k, v))
@ -145,6 +177,7 @@ def get_locale():
# CLI Classes Implementation -------------------------------------------
class TTYHandler(logging.StreamHandler):
"""TTY log handler
A handler class which prints logging records for a tty. The record is
@ -210,6 +243,7 @@ class TTYHandler(logging.StreamHandler):
class ActionsMapParser(BaseActionsMapParser):
"""Actions map's Parser for the CLI
Provide actions map parsing methods for a CLI usage. The parser for
@ -329,7 +363,7 @@ class ActionsMapParser(BaseActionsMapParser):
raise
except:
logger.exception("unable to parse arguments '%s'", ' '.join(args))
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
raise MoulinetteError('error_see_log')
else:
self.prepare_action_namespace(getattr(ret, '_tid', None), ret)
self._parser.dequeue_callbacks(ret)
@ -337,6 +371,7 @@ class ActionsMapParser(BaseActionsMapParser):
class Interface(BaseInterface):
"""Command-line Interface for the moulinette
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']:
raise MoulinetteError(errno.EINVAL, m18n.g('invalid_usage'))
raise MoulinetteError('invalid_usage')
# auto-complete
argcomplete.autocomplete(self.actionsmap.parser._parser)
@ -389,7 +424,7 @@ class Interface(BaseInterface):
try:
ret = self.actionsmap.process(args, timeout=timeout)
except (KeyboardInterrupt, EOFError):
raise MoulinetteError(errno.EINTR, m18n.g('operation_interrupted'))
raise MoulinetteError('operation_interrupted')
if ret is None or output_as == 'none':
return
@ -439,7 +474,7 @@ class Interface(BaseInterface):
if confirm:
m = message[0].lower() + message[1:]
if prompt(m18n.g('confirm', prompt=m)) != value:
raise MoulinetteError(errno.EINVAL, m18n.g('values_mismatch'))
raise MoulinetteError('values_mismatch')
return value

View file

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

View file

@ -70,6 +70,7 @@ def getHandlersByClass(classinfo, limit=0):
class MoulinetteLogger(Logger):
"""Custom logger class
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):
"""Extend log record for an optionnal action
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
from moulinette import m18n
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)
# Invalid URL
except requests.exceptions.ConnectionError:
raise MoulinetteError(errno.EBADE,
m18n.g('invalid_url', url=url))
raise MoulinetteError('invalid_url', url=url)
# SSL exceptions
except requests.exceptions.SSLError:
raise MoulinetteError(errno.EBADE,
m18n.g('download_ssl_error', url=url))
raise MoulinetteError('download_ssl_error', url=url)
# Timeout exceptions
except requests.exceptions.Timeout:
raise MoulinetteError(errno.ETIME,
m18n.g('download_timeout', url=url))
raise MoulinetteError('download_timeout', url=url)
# Unknown stuff
except Exception as e:
raise MoulinetteError(errno.ECONNRESET,
m18n.g('download_unknown_error',
url=url, error=str(e)))
raise MoulinetteError('download_unknown_error',
url=url, error=str(e))
# Assume error if status code is not 200 (OK)
if expected_status_code is not None \
and r.status_code != expected_status_code:
raise MoulinetteError(errno.EBADE,
m18n.g('download_bad_status_code',
url=url, code=str(r.status_code)))
raise MoulinetteError('download_bad_status_code',
url=url, code=str(r.status_code))
return r.text
@ -66,7 +59,6 @@ def download_json(url, timeout=30, expected_status_code=200):
try:
loaded_json = json.loads(text)
except ValueError:
raise MoulinetteError(errno.EINVAL,
m18n.g('corrupted_json', ressource=url))
raise MoulinetteError('corrupted_json', ressource=url)
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:
assert len(callback) == 3
stdinfo = kwargs.pop("stdinfo")
os.mkfifo(stdinfo, 0600)
os.mkfifo(stdinfo, 0o600)
# Open stdinfo for reading (in a nonblocking way, i.e. even
# if command does not write in the stdinfo pipe...)
stdinfo_f = os.open(stdinfo, os.O_RDONLY | os.O_NONBLOCK)
else:
kwargs.pop("stdinfo")
if "stdinfo" in kwargs:
kwargs.pop("stdinfo")
stdinfo = None
# Validate callback argument
@ -98,13 +99,15 @@ def call_async_output(args, callback, **kwargs):
# this way is not 100% perfect but should do it
stdout_consum.process_next_line()
stderr_consum.process_next_line()
stdinfo_consum.process_next_line()
if stdinfo:
stdinfo_consum.process_next_line()
time.sleep(.1)
stderr_reader.join()
# clear the queues
stdout_consum.process_current_queue()
stderr_consum.process_current_queue()
stdinfo_consum.process_current_queue()
if stdinfo:
stdinfo_consum.process_current_queue()
else:
while not stdout_reader.eof():
stdout_consum.process_current_queue()

View file

@ -1,6 +1,7 @@
import logging
from json.encoder import JSONEncoder
import datetime
import pytz
logger = logging.getLogger('moulinette.utils.serialize')
@ -8,6 +9,7 @@ logger = logging.getLogger('moulinette.utils.serialize')
# JSON utilities -------------------------------------------------------
class JSONExtendedEncoder(JSONEncoder):
"""Extended JSON encoder
Extend default JSON encoder to recognize more types and classes. It
@ -26,7 +28,9 @@ class JSONExtendedEncoder(JSONEncoder):
return list(o)
# 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 the repr for object that json can't encode

View file

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

View file

@ -30,5 +30,6 @@ setup(name='Moulinette',
'moulinette.interfaces',
'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