Merge branch 'stretch-unstable' into enh-python3

This commit is contained in:
Alexandre Aubin 2020-04-01 19:41:11 +02:00 committed by GitHub
commit 6e5b389e71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 7544 additions and 221 deletions

View file

@ -1,5 +1,11 @@
language: python
addons:
apt:
packages:
- ldap-utils
- slapd
matrix:
include:
- python: 3.5

14
debian/changelog vendored
View file

@ -1,3 +1,17 @@
moulinette (3.7.0.2) stable; urgency=low
Bumping version number for stable release
-- Kay0u <pierre@kayou.io> Thu, 26 Mar 2020 22:03:23 +0000
moulinette (3.7.0.1) testing; urgency=low
- [fix] Slapd may crash if we try to update the LDAP with no change (moulinette#231)
Thanks to all contributors (Josue) <3 !
-- Kay0u <pierre@kayou.io> Sun, 15 Mar 2020 16:09:25 +0000
moulinette (3.7.0) testing; urgency=low
# ~ Major stuff

View file

@ -13,7 +13,7 @@
"file_not_exist": "الملف غير موجود : '{path}'",
"folder_exists": "إنّ المجلد موجود من قبل : '{path}'",
"folder_not_exist": "المجلد غير موجود",
"instance_already_running": "هناك نسخة خادوم تشتغل مِن قبل",
"instance_already_running": "هناك بالفعل عملية YunoHost جارية. الرجاء الانتظار حتى ينتهي الأمر قبل تشغيل آخر.",
"invalid_argument": "المُعامِل غير صالح '{argument}': {error}",
"invalid_password": "كلمة السر خاطئة",
"invalid_usage": "إستعمال غير صالح، إستخدم --help لعرض المساعدة",
@ -52,5 +52,7 @@
"command_unknown": "الأمر '{command:s}' مجهول؟",
"corrupted_yaml": "قراءة مُشوّهة لنسق yaml مِن {ressource:s} (السبب : {error:s})",
"info": "معلومة:",
"warn_the_user_about_waiting_lock_again": "جارٍ الانتظار…"
"warn_the_user_about_waiting_lock_again": "جارٍ الانتظار…",
"warn_the_user_that_lock_is_acquired": "لقد انتهى تنفيذ ذاك الأمر ، جارٍ إطلاق الأمر",
"warn_the_user_about_waiting_lock": "هناك أمر لـ YunoHost قيد التشغيل حاليا. في انتظار انتهاء تنفيذه قبل تشغيل التالي"
}

View file

@ -1 +1,4 @@
{}
{
"logged_out": "প্রস্থান",
"password": "পাসওয়ার্ড"
}

View file

@ -13,7 +13,7 @@
"file_not_exist": "El fitxer no existeix: '{path}'",
"folder_exists": "La carpeta ja existeix: '{path}'",
"folder_not_exist": "La carpeta no existeix",
"instance_already_running": "Una instància ja s'està executant",
"instance_already_running": "Ja hi ha una operació de YunoHost en curs. Espereu a que s'acabi abans d'executar-ne una altra.",
"invalid_argument": "Argument invàlid '{argument}': {error}",
"invalid_password": "Contrasenya invàlida",
"invalid_usage": "Utilització invàlida, utilitzeu --help per veure l'ajuda",
@ -55,5 +55,6 @@
"corrupted_toml": "El fitxer TOML ha estat corromput en la lectura des de {ressource:s} (motiu: {error:s})",
"warn_the_user_about_waiting_lock": "Hi ha una altra ordre de YunoHost en execució, s'executarà aquesta ordre un cop l'anterior hagi acabat",
"warn_the_user_about_waiting_lock_again": "Encara en espera…",
"warn_the_user_that_lock_is_acquired": "l'altra ordre tot just ha acabat, ara s'executarà aquesta ordre"
"warn_the_user_that_lock_is_acquired": "l'altra ordre tot just ha acabat, ara s'executarà aquesta ordre",
"invalid_token": "Testimoni no vàlid - torneu-vos a autenticar"
}

View file

@ -1,5 +1,5 @@
{
"argument_required": "{argument}是必须的",
"argument_required": "参数“{argument}是必须的",
"authentication_profile_required": "必须验证配置文件{profile}",
"authentication_required": "需要验证",
"authentication_required_long": "此操作需要验证",
@ -22,7 +22,7 @@
"ldap_operation_error": "LDAP操作时发生了错误",
"ldap_server_down": "无法连接LDAP服务器",
"logged_in": "登录成功",
"logged_out": "注销成功",
"logged_out": "登出",
"not_logged_in": "您未登录",
"operation_interrupted": "操作中断",
"password": "密码",

View file

@ -19,7 +19,7 @@
"ldap_operation_error": "Ein Fehler trat während der LDAP Abfrage auf",
"ldap_server_down": "LDAP-Server nicht erreichbar",
"logged_in": "Angemeldet",
"logged_out": "Ausgeloggt",
"logged_out": "Abgemeldet",
"not_logged_in": "Du bist nicht angemeldet",
"operation_interrupted": "Vorgang unterbrochen",
"password": "Passwort",

View file

@ -1 +1,4 @@
{}
{
"logged_out": "Αποσυνδέθηκα",
"password": "Κωδικός πρόσβασης"
}

View file

@ -19,7 +19,7 @@
"invalid_token": "Invalid token - please authenticate",
"invalid_usage": "Invalid usage, pass --help to see help",
"ldap_attribute_already_exists": "Attribute '{attribute}' already exists with value '{value}'",
"ldap_operation_error": "An error occurred during LDAP operation",
"ldap_operation_error": "An error occurred during LDAP '{action}' operation",
"ldap_server_down": "Unable to reach LDAP server",
"logged_in": "Logged in",
"logged_out": "Logged out",
@ -55,5 +55,5 @@
"command_unknown": "Command '{command:s}' unknown ?",
"warn_the_user_about_waiting_lock": "Another YunoHost command is running right now, we are waiting for it to finish before running this one",
"warn_the_user_about_waiting_lock_again": "Still waiting...",
"warn_the_user_that_lock_is_acquired": "the other command just complet, now starting this command"
"warn_the_user_that_lock_is_acquired": "the other command just completed, now starting this command"
}

View file

@ -1,3 +1,59 @@
{
"password": "Pasvorto"
"password": "Pasvorto",
"colon": "{}: ",
"warn_the_user_that_lock_is_acquired": "la alia komando nur kompletigas, nun komencante ĉi tiun komandon",
"warn_the_user_about_waiting_lock_again": "Ankoraŭ atendanta...",
"warn_the_user_about_waiting_lock": "Alia komando de YunoHost funkcias ĝuste nun, ni atendas, ke ĝi finiĝos antaŭ ol funkcii ĉi tiu",
"command_unknown": "Komando '{command:s}' nekonata?",
"download_bad_status_code": "{url:s} redonita statuskodo {code:s}",
"download_unknown_error": "Eraro dum elŝutado de datumoj de {url:s}: {error:s}",
"download_timeout": "{url:s} prenis tro da tempo por respondi, rezignis.",
"download_ssl_error": "SSL-eraro dum konekto al {url:s}",
"invalid_url": "Nevalida url {url:s} (ĉu ĉi tiu retejo ekzistas?)",
"error_changing_file_permissions": "Eraro dum ŝanĝo de permesoj por {path:s}: {error:s}",
"error_removing": "Eraro dum la forigo de {path:s}: {error:s}",
"error_writing_file": "Eraro skribinte dosieron {file:s}: {error:s}",
"corrupted_toml": "Korupta toml legita el {ressource:s} (kialo: {error:s})",
"corrupted_yaml": "Korupta yaml legita de {ressource:s} (kialo: {error:s})",
"corrupted_json": "Koruptita json legita de {ressource:s} (kialo: {error:s})",
"unknown_error_reading_file": "Nekonata eraro dum provi legi dosieron {file:s} (kialo: {error:s})",
"cannot_write_file": "Ne povis skribi dosieron {file:s} (kialo: {error:s})",
"cannot_open_file": "Ne povis malfermi dosieron {file: s} (kialo: {error: s})",
"websocket_request_expected": "Atendis ret-peto",
"warning": "Averto:",
"values_mismatch": "Valoroj ne kongruas",
"unknown_user": "Nekonata uzanto '{user}'",
"unknown_group": "Nekonata grupo \"{group}\"",
"unable_retrieve_session": "Ne eblas retrovi la sesion ĉar '{exception}'",
"unable_authenticate": "Ne eblas aŭtentiĝi",
"success": "Sukceson!",
"server_already_running": "Servilo jam funkcias sur tiu haveno",
"root_required": "Vi devas esti 'root' por plenumi ĉi tiun agon",
"pattern_not_match": "Ne kongruas kun ŝablono",
"operation_interrupted": "Operacio interrompita",
"not_logged_in": "Vi ne estas ensalutinta",
"logged_in": "Ensalutinta",
"ldap_server_down": "Ne eblas atingi la servilon LDAP",
"ldap_operation_error": "Eraro okazis dum LDAP-operacio",
"ldap_attribute_already_exists": "Atributo '{attribute}' jam ekzistas kun valoro '{value}'",
"invalid_usage": "Nevalida uzado, preterpase '--help' por vidi helpon",
"invalid_password": "Nevalida pasvorto",
"invalid_argument": "Nevalida argumento '{argument}': {error}",
"instance_already_running": "Jam funkcias YunoHost-operacio. Bonvolu atendi, ke ĝi finiĝos antaŭ ol funkcii alia.",
"info": "informoj:",
"folder_not_exist": "Dosierujo ne ekzistas",
"folder_exists": "Dosierujo jam ekzistas: '{path}'",
"file_not_exist": "Dosiero ne ekzistas: '{path}'",
"file_exists": "Dosiero jam ekzistas: '{path}'",
"error_see_log": "Eraro okazis. Bonvolu vidi la protokolojn por detaloj, ili troviĝas en /var/log/yunohost/.",
"error": "Eraro:",
"deprecated_command_alias": "'{prog} {old}' malakceptas kaj estos forigita estonte, uzu anstataŭe '{prog} {new}'",
"deprecated_command": "'{prog} {command}' malakceptas kaj estos forigita estonte",
"confirm": "Konfirmu {prompt}",
"authentication_required_long": "Aŭtentigo necesas por plenumi ĉi tiun agon",
"authentication_required": "Aŭtentigo bezonata",
"authentication_profile_required": "Aŭtentigo al la profilo '{profile}' bezonata",
"argument_required": "Argumento '{argument}' estas bezonata",
"logged_out": "Ensalutinta",
"invalid_token": "Nevalida tokeno - bonvolu autentiki"
}

View file

@ -53,7 +53,8 @@
"corrupted_yaml": "Lectura corrupta de yaml desde {ressource:s} (motivo: {error:s})",
"info": "Información:",
"corrupted_toml": "Lectura corrupta de TOML desde {ressource:s} (motivo: {error:s})",
"warn_the_user_that_lock_is_acquired": "la otra orden ha terminado, iniciando esta orden ahora",
"warn_the_user_that_lock_is_acquired": "la otra orden recién terminó, iniciando esta orden ahora",
"warn_the_user_about_waiting_lock_again": "Aún esperando...",
"warn_the_user_about_waiting_lock": "Otra orden de YunoHost se está ejecutando ahora, estamos esperando a que termine antes de ejecutar esta"
"warn_the_user_about_waiting_lock": "Otra orden de YunoHost se está ejecutando ahora, estamos esperando a que termine antes de ejecutar esta",
"invalid_token": "Token invalido - vuelva a autenticarte"
}

View file

@ -1,3 +1,6 @@
{
"argument_required": "'{argument}' argumentua beharrezkoa da"
"argument_required": "'{argument}' argumentua beharrezkoa da",
"logged_out": "Saioa amaitu",
"password": "Pasahitza",
"colon": "{}: "
}

View file

@ -55,5 +55,6 @@
"corrupted_toml": "Fichier TOML corrompu en lecture depuis {ressource:s} (cause : {error:s})",
"warn_the_user_about_waiting_lock": "Une autre commande YunoHost est actuellement en cours, nous attendons qu'elle se termine avant de démarrer celle là",
"warn_the_user_about_waiting_lock_again": "Toujours en attente...",
"warn_the_user_that_lock_is_acquired": "l'autre commande vient de se terminer, lancement de cette commande"
"warn_the_user_that_lock_is_acquired": "l'autre commande vient de se terminer, lancement de cette commande",
"invalid_token": "Jeton non valide - veuillez vous authentifier"
}

View file

@ -1 +1,4 @@
{}
{
"logged_out": "Kilépett",
"password": "Jelszó"
}

View file

@ -15,7 +15,7 @@
"file_not_exist": "Il file non esiste: '{path}'",
"folder_exists": "La cartella esiste già: '{path}'",
"folder_not_exist": "La cartella non esiste",
"instance_already_running": "Un'istanza è già in esecuzione",
"instance_already_running": "Esiste già un'operazione YunoHost in esecuzione. Attendi il completamento prima di eseguirne un altro.",
"invalid_argument": "Argomento non valido '{argument}': {error}",
"invalid_password": "Password non valida",
"invalid_usage": "Utilizzo non valido, usa --help per vedere l'aiuto",
@ -31,7 +31,7 @@
"server_already_running": "Un server è già in esecuzione su quella porta",
"success": "Riuscito!",
"unable_authenticate": "Autenticazione fallita",
"unable_retrieve_session": "Recupero della sessione non riuscito",
"unable_retrieve_session": "Impossibile recuperare la sessione perché \"{exception}\"",
"unknown_group": "Gruppo '{group}' sconosciuto",
"unknown_user": "Utente '{user}' sconosciuto",
"values_mismatch": "I valori non corrispondono",
@ -39,7 +39,7 @@
"websocket_request_expected": "Richiesta WebSocket attesa",
"cannot_open_file": "Impossibile aprire il file {file:s} (motivo: {error:s})",
"cannot_write_file": "Impossibile scrivere il file {file:s} (motivo: {error:s})",
"unknown_error_reading_file": "Errore sconosciuto nel tentativo di leggere il file {file:s}",
"unknown_error_reading_file": "Errore sconosciuto durante il tentativo di leggere il file {file:s} (motivo: {errore:s})",
"corrupted_json": "Lettura json corrotta da {ressource:s} (motivo: {error:s})",
"corrupted_yaml": "Lettura yaml corrotta da {ressource:s} (motivo: {error:s})",
"error_writing_file": "Errore durante la scrittura del file {file:s}: {error:s}",
@ -51,5 +51,10 @@
"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 ?",
"info": "Info:"
"info": "Info:",
"warn_the_user_that_lock_is_acquired": "l'altro comando è appena completato, ora avvia questo comando",
"warn_the_user_about_waiting_lock_again": "Sto ancora aspettando ...",
"warn_the_user_about_waiting_lock": "Un altro comando YunoHost è in esecuzione in questo momento, stiamo aspettando che finisca prima di eseguire questo",
"corrupted_toml": "Toml corrotto da {ressource:s} (motivo: {errore:s})",
"invalid_token": "Token non valido: autenticare"
}

1
locales/ne.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -11,7 +11,7 @@
"file_not_exist": "Bestand bestaat niet: '{path}'",
"folder_exists": "Deze map bestaat al: '{path}'",
"folder_not_exist": "Map bestaat niet",
"instance_already_running": "Er is al een instantie actief",
"instance_already_running": "Er is al een instantie actief, bedankt om te wachten tot deze afgesloten is alvorens een andere te starten.",
"invalid_argument": "Ongeldig argument '{argument}': {error}",
"invalid_password": "Ongeldig wachtwoord",
"invalid_usage": "Ongeldig gebruik, doe --help om de hulptekst te lezen",
@ -29,7 +29,7 @@
"server_already_running": "Er is al een server actief op die poort",
"success": "Succes!",
"unable_authenticate": "Aanmelding niet mogelijk",
"unable_retrieve_session": "Kan de sessie niet ophalen",
"unable_retrieve_session": "Het is onmogelijk op de sessie op te halen omwille van '{exception}'",
"values_mismatch": "Waarden zijn niet gelijk",
"warning": "Waarschuwing:",
"websocket_request_expected": "Verwachtte een WebSocket request",
@ -39,7 +39,7 @@
"unknown_user": "Gebruiker '{user}' is onbekend",
"cannot_open_file": "Niet mogelijk om bestand {file:s} te openen (reden: {error:s})",
"cannot_write_file": "Niet gelukt om bestand {file:s} te schrijven (reden: {error:s})",
"unknown_error_reading_file": "Ongekende fout tijdens het lezen van bestand {file:s}",
"unknown_error_reading_file": "Ongekende fout tijdens het lezen van bestand {file:s} (cause:{error:s})",
"corrupted_json": "Corrupte json gelezen van {ressource:s} (reden: {error:s})",
"error_writing_file": "Fout tijdens het schrijven van bestand {file:s}: {error:s}",
"error_removing": "Fout tijdens het verwijderen van {path:s}: {error:s}",
@ -49,5 +49,12 @@
"download_timeout": "{url:s} neemt te veel tijd om te antwoorden, we geven het op.",
"download_unknown_error": "Fout tijdens het downloaden van data van {url:s}: {error:s}",
"download_bad_status_code": "{url:s} stuurt status code {code:s}",
"command_unknown": "Opdracht '{command:s}' ongekend ?"
"command_unknown": "Opdracht '{command:s}' ongekend ?",
"warn_the_user_that_lock_is_acquired": "het andere commando is net voltooid, starten van dit commando",
"warn_the_user_about_waiting_lock_again": "Nog steeds aan het wachten...",
"warn_the_user_about_waiting_lock": "Een ander YunoHost commando wordt uitgevoerd, we wachten tot het gedaan is alovrens dit te starten",
"corrupted_toml": "Ongeldige TOML werd gelezen op {ressource:s} (reason: {error:s})",
"corrupted_yaml": "Ongeldig YAML bestand op {ressource:s} (reason: {error:s})",
"invalid_token": "Ongeldig token - gelieve in te loggen",
"info": "Ter info:"
}

View file

@ -16,7 +16,7 @@
"file_not_exist": "Lo fichièr « {path} » existís pas",
"folder_exists": "Lo repertòri existís ja: « {path} »",
"folder_not_exist": "Lo repertòri existís pas",
"instance_already_running": "Una instància es ja en execucion",
"instance_already_running": "I a ja una operacion de YunoHost en cors. Mercés desperar que sacabe abans de ne lançar una mai.",
"invalid_argument": "Argument « {argument} » incorrècte: {error}",
"invalid_password": "Senhal incorrècte",
"ldap_server_down": "Impossible daténher lo servidor LDAP",
@ -55,5 +55,6 @@
"corrupted_toml": "Fichièr TOML corromput en lectura de {ressource:s} estant (rason: {error:s})",
"warn_the_user_about_waiting_lock": "Una autra comanda YunoHost es en execucion, sèm a esperar quacabe abans daviar aquesta daquí",
"warn_the_user_about_waiting_lock_again": "Encara en espèra…",
"warn_the_user_that_lock_is_acquired": "lautra comanda ven dacabar, lançament daquesta comanda"
"warn_the_user_that_lock_is_acquired": "lautra comanda ven dacabar, ara lançament daquesta comanda",
"invalid_token": "Geton invalid - volgatz vos autentificar"
}

View file

@ -1 +1,58 @@
{}
{
"logged_out": "Wylogowano",
"password": "hasło",
"warn_the_user_that_lock_is_acquired": "drugie polecenie właśnie się zakończyło, teraz uruchamia to polecenie",
"warn_the_user_about_waiting_lock_again": "Wciąż czekam...",
"warn_the_user_about_waiting_lock": "Kolejne polecenie YunoHost jest teraz uruchomione, czekamy na jego zakończenie przed uruchomieniem tego",
"command_unknown": "Polecenie „{command:s}” jest nieznane?",
"download_bad_status_code": "{url:s} zwrócił kod stanu {code:s}",
"download_unknown_error": "Błąd podczas pobierania danych z {url:s}: {error:s}",
"download_timeout": "{url:s} odpowiedział zbyt długo, poddał się.",
"download_ssl_error": "Błąd SSL podczas łączenia z {url:s}",
"invalid_url": "Nieprawidłowy adres URL {url:s} (czy ta strona istnieje?)",
"error_changing_file_permissions": "Błąd podczas zmiany uprawnień dla {path:s}: {error:s}",
"error_removing": "Błąd podczas usuwania {path:s}: {error:s}",
"error_writing_file": "Błąd podczas zapisywania pliku {file:s}: {error:s}",
"corrupted_toml": "Uszkodzony toml z {ressource: s} (powód: {error:s})",
"corrupted_yaml": "Uszkodzony yaml odczytany z {ressource:s} (powód: {error:s})",
"corrupted_json": "Uszkodzony json odczytany z {ressource:s} (powód: {error:s})",
"unknown_error_reading_file": "Nieznany błąd podczas próby odczytania pliku {file:s} (przyczyna: {error:s})",
"cannot_write_file": "Nie można zapisać pliku {file:s} (przyczyna: {error:s})",
"cannot_open_file": "Nie można otworzyć pliku {file:s} (przyczyna: {error:s})",
"websocket_request_expected": "Oczekiwano żądania WebSocket",
"warning": "Ostrzeżenie:",
"values_mismatch": "Wartości nie pasują",
"unknown_user": "Nieznany użytkownik „{user}”",
"unknown_group": "Nieznana grupa „{group}”",
"unable_retrieve_session": "Nie można pobrać sesji, ponieważ „{exception}”",
"unable_authenticate": "Nie można uwierzytelnić",
"success": "Sukces!",
"server_already_running": "Serwer już działa na tym porcie",
"root_required": "Aby wykonać tę akcję, musisz być rootem",
"pattern_not_match": "Nie pasuje do wzoru",
"operation_interrupted": "Operacja przerwana",
"not_logged_in": "Nie jesteś zalogowany",
"logged_in": "Zalogowany",
"ldap_server_down": "Nie można połączyć się z serwerem LDAP",
"ldap_operation_error": "Wystąpił błąd podczas operacji LDAP",
"ldap_attribute_already_exists": "Atrybut „{attribute}” już istnieje z wartością „{value}”",
"invalid_usage": "Nieprawidłowe użycie. Przejdź --help, aby wyświetlić pomoc",
"invalid_token": "Nieprawidłowy token - proszę uwierzytelnić",
"invalid_password": "Nieprawidłowe hasło",
"invalid_argument": "Nieprawidłowy argument „{argument}”: {error}",
"instance_already_running": "Trwa już operacja YunoHost. Zaczekaj na zakończenie, zanim uruchomisz kolejny.",
"info": "Informacje:",
"folder_not_exist": "Folder nie istnieje",
"folder_exists": "Folder już istnieje: „{path}”",
"file_not_exist": "Plik nie istnieje: „{path}”",
"file_exists": "Plik już istnieje: „{path}”",
"error_see_log": "Wystąpił błąd. Szczegółowe informacje można znaleźć w dziennikach, znajdują się one w katalogu /var/log/yunohost/.",
"error": "Błąd:",
"deprecated_command_alias": "„{prog} {old}” jest przestarzałe i zostanie usunięte w przyszłości, zamiast tego użyj „{prog} {new}”",
"deprecated_command": "„{prog} {command}” jest przestarzałe i zostanie usunięte w przyszłości",
"confirm": "Potwierdź {prompt}",
"colon": "{}: ",
"authentication_required_long": "Do wykonania tej czynności wymagane jest uwierzytelnienie",
"authentication_required": "Wymagane uwierzytelnienie",
"argument_required": "Argument „{argument}” jest wymagany"
}

View file

@ -10,7 +10,7 @@
"file_not_exist": "O ficheiro não existe: '{path}'",
"folder_exists": "A pasta já existe: '{path}'",
"folder_not_exist": "A pasta não existe",
"instance_already_running": "O serviço já está em execussão",
"instance_already_running": "Já existe uma operação YunoHost em execução. Aguarde o término antes de executar outro.",
"invalid_argument": "Argumento inválido '{argument}': {error}",
"invalid_password": "Senha incorreta",
"invalid_usage": "Uso invalido, utilizar --help para ver a ajuda",
@ -28,7 +28,7 @@
"server_already_running": "Existe um servidor ativo nessa porta",
"success": "Sucesso!",
"unable_authenticate": "Não foi possível autenticar",
"unable_retrieve_session": "Não foi possível recuperar a sessão",
"unable_retrieve_session": "Não foi possível recuperar a sessão porque '{exception}'",
"values_mismatch": "Os valores não coincidem",
"warning": "Aviso:",
"websocket_request_expected": "Esperado um pedido a WebSocket",
@ -39,7 +39,7 @@
"unknown_user": "Nome de utilizador '{user}' desconhecido",
"cannot_open_file": "Não foi possível abrir o arquivo {file:s} (reason: {error:s})",
"cannot_write_file": "Não foi possível abrir o arquivo {file:s} (reason: {error:s})",
"unknown_error_reading_file": "Erro desconhecido ao tentar ler o arquivo {file:s}",
"unknown_error_reading_file": "Erro desconhecido ao tentar ler o arquivo {file:s} (motivo: {error:s})",
"error_writing_file": "Erro ao gravar arquivo {file:s}: {error:s}",
"error_removing": "Erro ao remover {path:s}: {error:s}",
"error_changing_file_permissions": "Erro ao alterar as permissões para {path:s}: {error:s}",
@ -50,5 +50,11 @@
"download_bad_status_code": "{url:s} retornou o código de status {code:s}",
"command_unknown": "Comando '{command:s}' desconhecido ?",
"corrupted_json": "Json corrompido lido do {ressource:s} (motivo: {error:s})",
"corrupted_yaml": "Yaml corrompido lido do {ressource:s} (motivo: {error:s})"
"corrupted_yaml": "Yaml corrompido lido do {ressource:s} (motivo: {error:s})",
"warn_the_user_that_lock_is_acquired": "o outro comando acabou de concluir, agora iniciando este comando",
"warn_the_user_about_waiting_lock_again": "Ainda esperando...",
"warn_the_user_about_waiting_lock": "Outro comando YunoHost está sendo executado agora, estamos aguardando o término antes de executar este",
"corrupted_toml": "Toml corrompido lido em {ressource:s} (motivo: {error:s})",
"invalid_token": "Token inválido - autentique",
"info": "Informações:"
}

View file

@ -32,7 +32,7 @@
"websocket_request_expected": "Ожидается запрос WebSocket",
"cannot_open_file": "Не могу открыть файл {file:s} (причина: {error:s})",
"cannot_write_file": "Не могу записать файл {file:s} (причина: {error:s})",
"unknown_error_reading_file": "Неизвестная ошибка при чтении файла {file:s}",
"unknown_error_reading_file": "Неизвестная ошибка при попытке прочитать файл {file:s} (причина: {error:s})",
"corrupted_yaml": "Повреждённой yaml получен от {ressource:s} (причина: {error:s})",
"error_writing_file": "Ошибка при записи файла {file:s}: {error:s}",
"error_removing": "Ошибка при удалении {path:s}: {error:s}",
@ -40,9 +40,20 @@
"download_ssl_error": "Ошибка SSL при соединении с {url:s}",
"download_timeout": "Превышено время ожидания ответа от {url:s}.",
"download_unknown_error": "Ошибка при загрузке данных с {url:s} : {error:s}",
"instance_already_running": "Процесс уже запущен",
"instance_already_running": "Операция YunoHost уже запущена. Пожалуйста, подождите, пока он закончится, прежде чем запускать другой.",
"ldap_operation_error": "Ошибка в процессе работы LDAP",
"root_required": "Чтобы выполнить это действие, вы должны иметь права root",
"corrupted_json": "Повреждённый json получен от {ressource:s} (причина: {error:s})",
"command_unknown": "Команда '{command:s}' неизвестна ?"
"command_unknown": "Команда '{command:s}' неизвестна ?",
"warn_the_user_that_lock_is_acquired": "другая команда только что завершилась, теперь запускает эту команду",
"warn_the_user_about_waiting_lock_again": "Все еще жду...",
"warn_the_user_about_waiting_lock": "Сейчас запускается еще одна команда YunoHost, мы ждем ее завершения, прежде чем запустить эту",
"download_bad_status_code": "{url:s} вернул код состояния {code:s}",
"error_changing_file_permissions": "Ошибка при изменении разрешений для {path:s}: {error:s}",
"corrupted_toml": "Поврежденный том, прочитанный из {ressource:s} (причина: {error:s})",
"unable_retrieve_session": "Невозможно получить сеанс, так как '{exception}'",
"ldap_server_down": "Невозможно связаться с сервером LDAP",
"invalid_usage": "Неправильное использование, передайте --help, чтобы увидеть помощь",
"invalid_token": "Неверный токен - пожалуйста, авторизуйтесь",
"info": "Информация:"
}

View file

@ -1 +1,58 @@
{}
{
"warn_the_user_about_waiting_lock_again": "Väntar fortfarande …",
"download_bad_status_code": "{url:s} svarade med statuskod {code:s}",
"download_timeout": "Gav upp eftersom {url:s} tog för lång tid på sig att svara.",
"download_ssl_error": "Ett SSL-fel påträffades vid anslutning till {url:s}",
"cannot_write_file": "Kunde inte skriva till filen {file:s} (orsak: {error:s})",
"cannot_open_file": "Kunde inte öppna filen {file:s} (orsak: {error:s})",
"websocket_request_expected": "Förväntade en WebSocket-förfrågan",
"warning": "Varning:",
"values_mismatch": "Värdena stämmer inte överens",
"unknown_user": "Okänd användare '{user}'",
"unknown_group": "Okänd grupp '{group}'",
"success": "Lyckades!",
"server_already_running": "En server använder redan den porten",
"root_required": "Du måste vara inloggad som root för att utföra den här åtgärden",
"pattern_not_match": "Stämmer inte in på mönstret",
"operation_interrupted": "Behandling avbruten",
"not_logged_in": "Du är inte inloggad",
"logged_in": "Inloggad",
"ldap_attribute_already_exists": "Attributet '{attribute}' finns redan med värdet '{value}'",
"invalid_password": "Ogiltigt lösenord",
"invalid_argument": "Ogiltig parameter '{argument}': {error}",
"logged_out": "Utloggad",
"info": "Info:",
"folder_not_exist": "Katalogen finns inte",
"folder_exists": "Katalogen finns redan: '{path}'",
"file_not_exist": "Filen finns inte: '{path}'",
"file_exists": "Filen finns redan: '{path}'",
"error_see_log": "Ett fel har inträffat. Kolla gärna i loggfilerna för mer information, de finns i /var/log/yunohost/.",
"error": "Fel:",
"deprecated_command_alias": "'{prog} {old}' rekommenderas inte längre och kommer tas bort i framtiden, använd '{prog} {new}' istället",
"deprecated_command": "'{prog} {command}' rekommenderas inte längre och kommer tas bort i framtiden",
"confirm": "Bekräfta {prompt}",
"colon": "{}: ",
"argument_required": "Parametern '{argument}' krävs",
"password": "Lösenord",
"warn_the_user_that_lock_is_acquired": "det andra kommandot har bara slutförts, nu startar du det här kommandot",
"warn_the_user_about_waiting_lock": "Ett annat YunoHost-kommando körs just nu, vi väntar på att det ska slutföras innan det här körs",
"command_unknown": "Kommando '{command:s}' okänd?",
"download_unknown_error": "Fel vid nedladdning av data från {url:s}: {error:s}",
"invalid_url": "Ogiltig url {url:s} (finns den här webbplatsen?)",
"error_changing_file_permissions": "Fel vid ändring av behörigheter för {path:s}: {error:s}",
"error_removing": "Fel vid borttagning av {path:s}: {error:s}",
"error_writing_file": "Fel vid skrivning av fil {file:s}: {error:s}",
"corrupted_toml": "Korrupt toml läst från {ressource:s} (anledning: {error:s})",
"corrupted_yaml": "Skadad yaml läst från {ressource:s} (anledning: {error:s})",
"corrupted_json": "Skadad json läst från {ressource:s} (anledning: {error:s})",
"unknown_error_reading_file": "Okänt fel vid försök att läsa filen {file:s} (anledning: {error:s})",
"unable_retrieve_session": "Det gick inte att hämta sessionen eftersom '{exception}'",
"unable_authenticate": "Det går inte att verifiera",
"ldap_server_down": "Det går inte att nå LDAP-servern",
"ldap_operation_error": "Ett fel inträffade under LDAP-drift",
"invalid_usage": "Ogiltig användning, pass --help för att se hjälp",
"invalid_token": "Ogiltigt token - verifiera",
"instance_already_running": "Det finns redan en YunoHost-operation. Vänta tills den är klar innan du kör en annan.",
"authentication_required_long": "Autentisering krävs för att utföra denna åtgärd",
"authentication_required": "Autentisering krävs"
}

View file

@ -6,8 +6,8 @@
"colon": "{}: ",
"confirm": "{prompt}'i doğrulayın",
"error": "Hata:",
"error_see_log": "Bir hata oluştu. Detaylar için lütfen loga bakınız",
"instance_already_running": "Uygulama zaten çalışıyor",
"error_see_log": "Bir hata oluştu. Ayrıntılar için lütfen günlüklere bakın, bunlar /var/log/yunohost/ dizinindedir.",
"instance_already_running": "Halihazırda bir YunoHost operasyonu var. Lütfen başka bir tane çalıştırmadan önce bitmesini bekleyin.",
"invalid_argument": "Geçersiz argüman '{argument}': {error}",
"invalid_password": "Geçersiz parola",
"ldap_attribute_already_exists": "'{attribute}={value}' özelliği zaten mevcut",
@ -24,8 +24,37 @@
"server_already_running": "Bu portta zaten çalışan bir sunucu var",
"success": "İşlem Başarılı!",
"unable_authenticate": "Yetkilendirme başarısız",
"unable_retrieve_session": "Oturum bilgileri alınamadı",
"unable_retrieve_session": "'{exception}' nedeniyle oturum alınamadı",
"values_mismatch": "Değerler uyuşmuyor",
"warning": "Uyarı:",
"websocket_request_expected": "WebSocket isteği gerekli"
"websocket_request_expected": "WebSocket isteği gerekli",
"warn_the_user_that_lock_is_acquired": "diğer komut şimdi tamamlandı, şimdi bu komutu başlatıyor",
"warn_the_user_about_waiting_lock_again": "Hala bekliyor...",
"warn_the_user_about_waiting_lock": "Başka bir YunoHost komutu şu anda çalışıyor, bunu çalıştırmadan önce bitmesini bekliyoruz",
"command_unknown": "'{Command:s}' komutu bilinmiyor mu?",
"download_bad_status_code": "{url:s} döndürülen durum kodu {code:s}",
"download_unknown_error": "{url:s} adresinden veri indirilirken hata oluştu: {error:s}",
"download_timeout": "{url:s} yanıtlaması çok uzun sürdü, pes etti.",
"download_ssl_error": "{url:s} ağına bağlanırken SSL hatası",
"invalid_url": "Geçersiz url {url:s} (bu site var mı?)",
"error_changing_file_permissions": "{Path:s} için izinler değiştirilirken hata oluştu: {error:s}",
"error_removing": "{Path:s} kaldırılırken hata oluştu: {error:s}",
"error_writing_file": "{File:s} dosyası yazılırken hata oluştu: {error:s}",
"corrupted_toml": "{Ressource:s} kaynağından okunan bozuk toml (nedeni: {hata:s})",
"corrupted_yaml": "{Ressource:s} kaynağından bozuk yaml okunuyor (nedeni: {error:s})",
"corrupted_json": "{Ressource:s} adresinden okunan bozuk json (nedeni: {error:s})",
"unknown_error_reading_file": "{File:s} dosyasını okumaya çalışırken bilinmeyen hata (nedeni: {error:s})",
"cannot_write_file": "{File:s} dosyası yazılamadı (nedeni: {error:s})",
"cannot_open_file": "{File:s} dosyasıılamadı (nedeni: {error:s})",
"unknown_user": "Bilinmeyen '{user}' kullanıcı",
"unknown_group": "Bilinmeyen '{group}' grubu",
"invalid_usage": "Geçersiz kullanım, yardım görmek için --help iletin",
"invalid_token": "Geçersiz simge - lütfen kimlik doğrulaması yapın",
"info": "Bilgi:",
"folder_not_exist": "Klasör mevcut değil",
"folder_exists": "Klasör zaten var: '{path}'",
"file_not_exist": "Dosya mevcut değil: '{path}'",
"file_exists": "Dosya zaten var: '{path}'",
"deprecated_command_alias": "'{prog} {old}' kullanımdan kaldırıldı ve gelecekte kaldırılacak, bunun yerine '{prog} {new}' kullanın",
"deprecated_command": "'{prog} {command}' kullanımdan kaldırıldı ve gelecekte kaldırılacak"
}

View file

@ -33,6 +33,7 @@ __all__ = [
"api",
"cli",
"m18n",
"msignals",
"env",
"init_interface",
"MoulinetteError",

View file

@ -89,6 +89,8 @@ class CommentParameter(_ExtraParameter):
skipped_iface = ["api"]
def __call__(self, message, arg_name, arg_value):
if arg_value:
return
return msignals.display(m18n.n(message))
@classmethod
@ -191,7 +193,7 @@ class PatternParameter(_ExtraParameter):
v = arg_value
if v and not re.match(pattern, v or "", re.UNICODE):
logger.debug(
logger.warning(
"argument value '%s' for '%s' doesn't match pattern '%s'",
v,
arg_name,
@ -233,14 +235,14 @@ class RequiredParameter(_ExtraParameter):
def __call__(self, required, arg_name, arg_value):
if required and (arg_value is None or arg_value == ""):
logger.debug("argument '%s' is required", arg_name)
logger.warning("argument '%s' is required", arg_name)
raise MoulinetteError("argument_required", argument=arg_name)
return arg_value
@staticmethod
def validate(value, arg_name):
if not isinstance(value, bool):
raise TypeError("parameter value must be a list, got %r" % value)
raise TypeError("parameter value must be a boolean, got %r" % value)
return value

View file

@ -32,9 +32,11 @@ class BaseAuthenticator(object):
"""
def __init__(self, name):
def __init__(self, name, vendor, parameters, extra):
self._name = name
self.vendor = vendor
self.is_authenticated = False
self.extra = extra
@property
def name(self):

View file

@ -18,11 +18,12 @@ class Authenticator(BaseAuthenticator):
def __init__(self, name, vendor, parameters, extra):
logger.debug("initialize authenticator dummy")
super(Authenticator, self).__init__(name)
def authenticate(self, password):
super(Authenticator, self).__init__(name, vendor, parameters, extra)
if not password == "Yoloswag":
raise MoulinetteError("Invalid password!")
def authenticate(self, password=None):
if not password == self.name:
raise MoulinetteError("invalid_password")
return self

View file

@ -39,18 +39,20 @@ class Authenticator(BaseAuthenticator):
self.basedn = parameters["base_dn"]
self.userdn = parameters["user_rdn"]
self.extra = extra
self.sasldn = "cn=external,cn=auth"
self.adminuser = "admin"
self.admindn = "cn=%s,dc=yunohost,dc=org" % self.adminuser
logger.debug(
"initialize authenticator '%s' with: uri='%s', "
"base_dn='%s', user_rdn='%s'",
name,
self.uri,
self._get_uri(),
self.basedn,
self.userdn,
)
super(Authenticator, self).__init__(name)
super(Authenticator, self).__init__(name, vendor, parameters, extra)
if self.userdn:
if "cn=external,cn=auth" in self.userdn:
if self.userdn and self.sasldn in self.userdn:
self.authenticate(None)
else:
self.con = None
@ -66,13 +68,13 @@ class Authenticator(BaseAuthenticator):
# Implement virtual methods
def authenticate(self, password):
def authenticate(self, password=None):
try:
con = ldap.ldapobject.ReconnectLDAPObject(
self.uri, retry_max=10, retry_delay=0.5
self._get_uri(), retry_max=10, retry_delay=0.5
)
if self.userdn:
if "cn=external,cn=auth" in self.userdn:
if self.sasldn in self.userdn:
con.sasl_non_interactive_bind_s("EXTERNAL")
else:
con.simple_bind_s(self.userdn, password)
@ -86,12 +88,14 @@ class Authenticator(BaseAuthenticator):
# Check that we are indeed logged in with the right identity
try:
who = con.whoami_s()
# whoami_s return dn:..., then delete these 3 characters
who = con.whoami_s()[3:]
except Exception as e:
logger.warning("Error during ldap authentication process: %s", e)
raise
else:
if who[3:] != self.userdn:
# FIXME: During SASL bind whoami from the test server return the admindn while userdn is returned normally :
if not (who == self.admindn or who == self.userdn):
raise MoulinetteError("Not logged in with the expected userdn ?!")
else:
self.con = con
@ -107,9 +111,7 @@ class Authenticator(BaseAuthenticator):
salt = "$6$" + salt + "$"
return "{CRYPT}" + crypt.crypt(str(password), salt)
hashed_password = self.search(
"cn=admin,dc=yunohost,dc=org", attrs=["userPassword"]
)[0]
hashed_password = self.search(self.admindn, attrs=["userPassword"])[0]
# post-install situation, password is not already set
if "userPassword" not in hashed_password or not hashed_password["userPassword"]:
@ -117,7 +119,10 @@ class Authenticator(BaseAuthenticator):
# we aren't using sha-512 but something else that is weaker, proceed to upgrade
if not hashed_password["userPassword"][0].startswith("{CRYPT}$6$"):
self.update("cn=admin", {"userPassword": _hash_user_password(password),})
self.update(
"cn=%s" % self.adminuser,
{"userPassword": [_hash_user_password(password)]},
)
# Additional LDAP methods
# TODO: Review these methods
@ -151,7 +156,7 @@ class Authenticator(BaseAuthenticator):
attrs,
e,
)
raise MoulinetteError("ldap_operation_error")
raise MoulinetteError("ldap_operation_error", action="search")
result_list = []
if not attrs or "dn" not in attrs:
@ -187,7 +192,7 @@ class Authenticator(BaseAuthenticator):
attr_dict,
e,
)
raise MoulinetteError("ldap_operation_error")
raise MoulinetteError("ldap_operation_error", action="add")
else:
return True
@ -211,7 +216,7 @@ class Authenticator(BaseAuthenticator):
rdn,
e,
)
raise MoulinetteError("ldap_operation_error")
raise MoulinetteError("ldap_operation_error", action="remove")
else:
return True
@ -232,10 +237,15 @@ class Authenticator(BaseAuthenticator):
actual_entry = self.search(base=dn, attrs=None)
ldif = modlist.modifyModlist(actual_entry[0], attr_dict, ignore_oldexistent=1)
if ldif == []:
logger.warning("Nothing to update in LDAP")
return True
try:
if new_rdn:
self.con.rename_s(dn, new_rdn)
dn = new_rdn + "," + self.basedn
new_base = dn.split(",", 1)[1]
dn = new_rdn + "," + new_base
self.con.modify_ext_s(dn, ldif)
except Exception as e:
@ -247,7 +257,7 @@ class Authenticator(BaseAuthenticator):
new_rdn,
e,
)
raise MoulinetteError("ldap_operation_error")
raise MoulinetteError("ldap_operation_error", action="update")
else:
return True
@ -284,7 +294,7 @@ class Authenticator(BaseAuthenticator):
value_dict -- Dictionnary of attributes/values to check
Returns:
None | list with Fist conflict attribute name and value
None | tuple with Fist conflict attribute name and value
"""
for attr, value in value_dict.items():
@ -293,3 +303,6 @@ class Authenticator(BaseAuthenticator):
else:
return (attr, value)
return None
def _get_uri(self):
return self.uri

View file

@ -451,12 +451,14 @@ class MoulinetteLock(object):
"""
base_lockfile = "/var/run/moulinette_%s.lock"
def __init__(self, namespace, timeout=None, interval=0.5):
self.namespace = namespace
self.timeout = timeout
self.interval = interval
self._lockfile = "/var/run/moulinette_%s.lock" % namespace
self._lockfile = self.base_lockfile % namespace
self._stale_checked = False
self._locked = False

View file

@ -55,7 +55,7 @@ class BaseActionsMapParser(object):
# Each parser classes must implement these methods.
@staticmethod
def format_arg_names(self, name, full):
def format_arg_names(name, full):
"""Format argument name
Format agument name depending on its 'full' parameter and return
@ -70,9 +70,7 @@ class BaseActionsMapParser(object):
A list of option strings
"""
raise NotImplementedError(
"derived class '%s' must override this method" % self.__class__.__name__
)
raise NotImplementedError("derived class must override this method")
def has_global_parser(self):
return False
@ -156,7 +154,8 @@ class BaseActionsMapParser(object):
# Arguments helpers
def prepare_action_namespace(self, tid, namespace=None):
@staticmethod
def prepare_action_namespace(tid, namespace=None):
"""Prepare the namespace for a given action"""
# Validate tid and namespace
if not isinstance(tid, tuple) and (
@ -245,6 +244,9 @@ class BaseActionsMapParser(object):
elif ifaces is False:
conf["authenticate"] = False
elif isinstance(ifaces, list):
if "all" in ifaces:
conf["authenticate"] = "all"
else:
# Store only if authentication is needed
conf["authenticate"] = True if self.interface in ifaces else False
else:
@ -256,27 +258,22 @@ class BaseActionsMapParser(object):
raise MoulinetteError("error_see_log")
# -- 'authenticator'
try:
auth = configuration["authenticator"]
except KeyError:
pass
else:
auth = configuration.get("authenticator", "default")
if not is_global and isinstance(auth, str):
try:
# Store needed authenticator profile
conf["authenticator"] = self.global_conf["authenticator"][auth]
except KeyError:
if auth not in self.global_conf["authenticator"]:
logger.error(
"requesting profile '%s' which is undefined in "
"global configuration of 'authenticator'",
auth,
)
raise MoulinetteError("error_see_log")
else:
conf["authenticator"] = auth
elif is_global and isinstance(auth, dict):
if len(auth) == 0:
logger.warning(
"no profile defined in global configuration "
"for 'authenticator'"
"no profile defined in global configuration " "for 'authenticator'"
)
else:
auths = {}

View file

@ -256,10 +256,8 @@ class _ActionsMapPlugin(object):
kwargs["password"] = request.POST["password"]
except KeyError:
raise HTTPBadRequestResponse("Missing password parameter")
try:
kwargs["profile"] = request.POST["profile"]
except KeyError:
pass
kwargs["profile"] = request.POST.get("profile", "default")
return callback(**kwargs)
return wrapper
@ -351,7 +349,7 @@ class _ActionsMapPlugin(object):
# Routes callbacks
def login(self, password, profile="default"):
def login(self, password, profile):
"""Log in to an authenticator profile
Attempt to authenticate to a given authenticator profile and
@ -406,13 +404,16 @@ class _ActionsMapPlugin(object):
"""
s_id = request.get_cookie("session.id")
try:
# We check that there's a (signed) session.hash available
# for additional security ?
# (An attacker could not craft such signed hashed ? (FIXME : need to make sure of this))
try:
s_secret = self.secrets[s_id]
request.get_cookie("session.tokens", secret=s_secret, default={})[profile]
except KeyError:
s_secret = {}
if profile not in request.get_cookie(
"session.tokens", secret=s_secret, default={}
):
raise HTTPUnauthorizedResponse(m18n.g("not_logged_in"))
else:
del self.secrets[s_id]
@ -661,24 +662,28 @@ class ActionsMapParser(BaseActionsMapParser):
# Return the created parser
return parser
def auth_required(self, args, route, **kwargs):
def auth_required(self, args, **kwargs):
try:
# Retrieve the tid for the route
tid, _ = self._parsers[route]
if not self.get_conf(tid, "authenticate"):
return False
else:
# TODO: In the future, we could make the authentication
# dependent of the route being hit ...
# e.g. in the context of friend2friend stuff that could
# auth with some custom auth system to access some
# data with something like :
# return self.get_conf(tid, 'authenticator')
return "default"
tid, _ = self._parsers[kwargs.get("route")]
except KeyError:
logger.error("no argument parser found for route '%s'", route)
logger.error("no argument parser found for route '%s'", kwargs.get("route"))
raise MoulinetteError("error_see_log")
if self.get_conf(tid, "authenticate"):
authenticator = self.get_conf(tid, "authenticator")
# If several authenticator, use the default one
if isinstance(authenticator, dict):
if "default" in authenticator:
authenticator = "default"
else:
# TODO which one should we use?
pass
return authenticator
else:
return False
def parse_args(self, args, route, **kwargs):
"""Parse arguments

View file

@ -287,7 +287,7 @@ class ActionsMapParser(BaseActionsMapParser):
@staticmethod
def format_arg_names(name, full):
if name[0] == "-" and full:
if name.startswith("-") and full:
return [name, full]
return [name]
@ -379,7 +379,16 @@ class ActionsMapParser(BaseActionsMapParser):
tid = getattr(ret, "_tid", None)
if self.get_conf(tid, "authenticate"):
return self.get_conf(tid, "authenticator")
authenticator = self.get_conf(tid, "authenticator")
# If several authenticator, use the default one
if isinstance(authenticator, dict):
if "default" in authenticator:
authenticator = "default"
else:
# TODO which one should we use?
pass
return authenticator
else:
return False
@ -446,6 +455,9 @@ class Interface(BaseInterface):
# Set handler for authentication
if password:
msignals.set_handler("authenticate", lambda a: a(password=password))
else:
if os.isatty(1):
msignals.set_handler("authenticate", self._do_authenticate)
try:
ret = self.actionsmap.process(args, timeout=timeout)

View file

@ -100,9 +100,7 @@ def read_toml(file_path):
try:
loaded_toml = toml.loads(file_content, _dict=OrderedDict)
except Exception as e:
raise MoulinetteError(
errno.EINVAL, m18n.g("corrupted_toml", ressource=file_path, error=str(e))
)
raise MoulinetteError("corrupted_toml", ressource=file_path, error=str(e))
return loaded_toml
@ -255,7 +253,7 @@ def write_to_yaml(file_path, data):
raise MoulinetteError("error_writing_file", file=file_path, error=str(e))
def mkdir(path, mode=0o777, parents=False, uid=None, gid=None, force=False):
def mkdir(path, mode=0o0777, 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
@ -290,7 +288,9 @@ def mkdir(path, mode=0o777, parents=False, uid=None, gid=None, force=False):
# Create directory and set permissions
try:
oldmask = os.umask(000)
os.mkdir(path, mode)
os.umask(oldmask)
except OSError:
# mimic Python3.2+ os.makedirs exist_ok behaviour
if not force or not os.path.isdir(path):

View file

@ -16,6 +16,16 @@ from logging import (
CRITICAL,
)
__all__ = [
"NOTSET", # noqa
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"CRITICAL",
"SUCCESS",
]
# Global configuration and functions -----------------------------------
@ -35,7 +45,7 @@ DEFAULT_LOGGING = {
"stream": "ext://sys.stdout",
},
},
"loggers": {"moulinette": {"level": "DEBUG", "handlers": ["console"],},},
"loggers": {"moulinette": {"level": "DEBUG", "handlers": ["console"]}},
}

View file

@ -22,12 +22,12 @@ def download_text(url, timeout=30, expected_status_code=200):
# Download file
try:
r = requests.get(url, timeout=timeout)
# Invalid URL
except requests.exceptions.ConnectionError:
raise MoulinetteError("invalid_url", url=url)
# SSL exceptions
except requests.exceptions.SSLError:
raise MoulinetteError("download_ssl_error", url=url)
# Invalid URL
except requests.exceptions.ConnectionError:
raise MoulinetteError("invalid_url", url=url)
# Timeout exceptions
except requests.exceptions.Timeout:
raise MoulinetteError("download_timeout", url=url)

View file

@ -43,9 +43,16 @@ class AsynchronousFileReader(Process):
else:
data = ""
while True:
try:
# Try to read (non-blockingly) a few bytes, append them to
# the buffer
data += os.read(self._fd, 50)
except Exception as e:
print(
"from moulinette.utils.stream: could not read file descriptor : %s"
% str(e)
)
continue
# If nobody's writing in there anymore, get out
if not data and os.fstat(self._fd).st_nlink == 0:

View file

@ -34,6 +34,9 @@ setup(name='Moulinette',
'pytz',
'pyyaml',
'toml',
'python-ldap',
'gevent-websocket',
'bottle',
],
tests_require=[
'pytest',
@ -42,5 +45,6 @@ setup(name='Moulinette',
'pytest-mock',
'requests',
'requests-mock',
'webtest'
],
)

View file

@ -13,13 +13,34 @@ _global:
yoloswag:
vendor: dummy
help: Dummy Yoloswag Password
ldap:
vendor: ldap
help: admin_password
parameters:
uri: ldap://localhost:8080
base_dn: dc=yunohost,dc=org
user_rdn: cn=admin,dc=yunohost,dc=org
arguments:
-v:
full: --version
help: Display Yoloswag versions
action: callback
callback:
method: test.src.testauth.yoloswag_version
return: true
-w:
full: --wersion
help: Not existing function
action: callback
callback:
method: test.src.testauth.not_existing_function
return: true
#############################
# Test Actions #
#############################
testauth:
actions:
none:
api: GET /test-auth/none
configuration:
@ -27,16 +48,76 @@ testauth:
default:
api: GET /test-auth/default
configuration:
authenticate: all
authenticator: default
# only-api:
# api: GET /test-auth/only-api
# configuration:
# authenticate: api
#
only-api:
api: GET /test-auth/only-api
configuration:
authenticate:
- api
only-cli:
api: GET /test-auth/only-cli
configuration:
authenticate:
- cli
other-profile:
api: GET /test-auth/other-profile
configuration:
authenticate:
- all
authenticator: yoloswag
ldap:
api: GET /test-auth/ldap
configuration:
authenticate:
- all
authenticator: ldap
with_arg:
api: GET /test-auth/with_arg/<super_arg>
arguments:
super_arg:
help: Super Arg
with_extra_str_only:
api: GET /test-auth/with_extra_str_only/<only_a_str>
arguments:
only_a_str:
help: Only a String
extra:
pattern:
- !!str ^[a-zA-Z]
- "pattern_only_a_str"
with_type_int:
api: GET /test-auth/with_type_int/<only_an_int>
arguments:
only_an_int:
help: Only an Int
type: int
subcategories:
subcat:
actions:
none:
api: GET /test-auth/subcat/none
configuration:
authenticate: false
default:
api: GET /test-auth/subcat/default
post:
api: POST /test-auth/subcat/post
configuration:
authenticate:
- all
authenticator: default
other-profile:
api: GET /test-auth/subcat/other-profile
configuration:
authenticator: yoloswag

View file

@ -1,10 +1,14 @@
"""Pytest fixtures for testing."""
import toml
import yaml
import json
import os
import shutil
import pytest
from src.ldap_server import LDAPServer
def patch_init(moulinette):
"""Configure moulinette to use the YunoHost namespace."""
@ -53,7 +57,7 @@ def patch_logging(moulinette):
"format": "%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s" # noqa
},
},
"filters": {"action": {"()": "moulinette.utils.log.ActionFilter",},},
"filters": {"action": {"()": "moulinette.utils.log.ActionFilter"}},
"handlers": {
"api": {
"level": level,
@ -66,17 +70,21 @@ def patch_logging(moulinette):
},
},
"loggers": {
"moulinette": {"level": level, "handlers": [], "propagate": True,},
"moulinette": {"level": level, "handlers": [], "propagate": True},
"moulinette.interface": {
"level": level,
"handlers": handlers,
"propagate": False,
},
},
"root": {"level": level, "handlers": root_handlers,},
"root": {"level": level, "handlers": root_handlers},
}
def patch_lock(moulinette):
moulinette.core.MoulinetteLock.base_lockfile = "moulinette_%s.lock"
@pytest.fixture(scope="session", autouse=True)
def moulinette(tmp_path_factory):
import moulinette
@ -96,6 +104,7 @@ def moulinette(tmp_path_factory):
patch_init(moulinette)
patch_translate(moulinette)
patch_lock(moulinette)
logging = patch_logging(moulinette)
moulinette.init(logging_config=logging, _from_source=False)
@ -125,6 +134,33 @@ def moulinette_webapi(moulinette):
return TestApp(moulinette_webapi._app)
@pytest.fixture
def moulinette_cli(moulinette, mocker):
# Dirty hack needed, otherwise cookies ain't reused between request .. not
# sure why :|
import argparse
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
"--debug",
action="store_true",
default=False,
help="Log and print debug messages",
)
mocker.patch("os.isatty", return_value=True)
moulinette_cli = moulinette.core.init_interface(
"cli",
actionsmap={
"namespaces": ["moulitest"],
"use_cache": False,
"parser_kwargs": {"top_parser": parser},
},
)
mocker.stopall()
return moulinette_cli
@pytest.fixture
def test_file(tmp_path):
test_text = "foo\nbar\n"
@ -141,6 +177,41 @@ def test_json(tmp_path):
return test_file
@pytest.fixture
def test_yaml(tmp_path):
test_yaml = yaml.dump({"foo": "bar"})
test_file = tmp_path / "test.txt"
test_file.write_bytes(test_yaml)
return test_file
@pytest.fixture
def test_toml(tmp_path):
test_toml = toml.dumps({"foo": "bar"})
test_file = tmp_path / "test.txt"
test_file.write_bytes(str(test_toml))
return test_file
@pytest.fixture
def test_ldif(tmp_path):
test_file = tmp_path / "test.txt"
from ldif import LDIFWriter
writer = LDIFWriter(open(str(test_file), "wb"))
writer.unparse(
"mail=alice@example.com",
{
"cn": ["Alice Alison"],
"mail": ["alice@example.com"],
"objectclass": ["top", "person"],
},
)
return test_file
@pytest.fixture
def user():
return os.getlogin()
@ -149,3 +220,11 @@ def user():
@pytest.fixture
def test_url():
return "https://some.test.url/yolo.txt"
@pytest.fixture
def ldap_server():
server = LDAPServer()
server.start()
yield server
server.stop()

View file

View file

@ -0,0 +1,84 @@
parents:
ou=users:
ou: users
objectClass:
- organizationalUnit
- top
ou=domains:
ou: domains
objectClass:
- organizationalUnit
- top
ou=apps:
ou: apps
objectClass:
- organizationalUnit
- top
ou=permission:
ou: permission
objectClass:
- organizationalUnit
- top
ou=groups:
ou: groups
objectClass:
- organizationalUnit
- top
ou=sudo:
ou: sudo
objectClass:
- organizationalUnit
- top
children:
cn=admin,ou=sudo:
cn: admin
sudoUser: admin
sudoHost: ALL
sudoCommand: ALL
sudoOption: "!authenticate"
objectClass:
- sudoRole
- top
cn=admins,ou=groups:
cn: admins
gidNumber: "4001"
memberUid: admin
objectClass:
- posixGroup
- top
cn=all_users,ou=groups:
cn: all_users
gidNumber: "4002"
objectClass:
- posixGroup
- groupOfNamesYnh
cn=visitors,ou=groups:
cn: visitors
gidNumber: "4003"
objectClass:
- posixGroup
- groupOfNamesYnh
depends_children:
cn=mail.main,ou=permission:
cn: mail.main
gidNumber: "5001"
objectClass:
- posixGroup
- permissionYnh
groupPermission:
- "cn=all_users,ou=groups,dc=yunohost,dc=org"
cn=xmpp.main,ou=permission:
cn: xmpp.main
gidNumber: "5002"
objectClass:
- posixGroup
- permissionYnh
groupPermission:
- "cn=all_users,ou=groups,dc=yunohost,dc=org"

View file

@ -0,0 +1,610 @@
# OpenLDAP Core schema
# $OpenLDAP$
## This work is part of OpenLDAP Software <http://www.openldap.org/>.
##
## Copyright 1998-2019 The OpenLDAP Foundation.
## All rights reserved.
##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted only as authorized by the OpenLDAP
## Public License.
##
## A copy of this license is available in the file LICENSE in the
## top-level directory of the distribution or, alternatively, at
## <http://www.OpenLDAP.org/license.html>.
#
## Portions Copyright (C) The Internet Society (1997-2006).
## All Rights Reserved.
##
## This document and translations of it may be copied and furnished to
## others, and derivative works that comment on or otherwise explain it
## or assist in its implementation may be prepared, copied, published
## and distributed, in whole or in part, without restriction of any
## kind, provided that the above copyright notice and this paragraph are
## included on all such copies and derivative works. However, this
## document itself may not be modified in any way, such as by removing
## the copyright notice or references to the Internet Society or other
## Internet organizations, except as needed for the purpose of
## developing Internet standards in which case the procedures for
## copyrights defined in the Internet Standards process must be
## followed, or as required to translate it into languages other than
## English.
##
## The limited permissions granted above are perpetual and will not be
## revoked by the Internet Society or its successors or assigns.
##
## This document and the information contained herein is provided on an
## "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING
## TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
## BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION
## HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF
## MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
#
#
# Includes LDAPv3 schema items from:
# RFC 2252/2256 (LDAPv3)
#
# Select standard track schema items:
# RFC 1274 (uid/dc)
# RFC 2079 (URI)
# RFC 2247 (dc/dcObject)
# RFC 2587 (PKI)
# RFC 2589 (Dynamic Directory Services)
# RFC 4524 (associatedDomain)
#
# Select informational schema items:
# RFC 2377 (uidObject)
#
# Standard attribute types from RFC 2256
#
# system schema
#attributetype ( 2.5.4.0 NAME 'objectClass'
# DESC 'RFC2256: object classes of the entity'
# EQUALITY objectIdentifierMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )
# system schema
#attributetype ( 2.5.4.1 NAME ( 'aliasedObjectName' 'aliasedEntryName' )
# DESC 'RFC2256: name of aliased object'
# EQUALITY distinguishedNameMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )
attributetype ( 2.5.4.2 NAME 'knowledgeInformation'
DESC 'RFC2256: knowledge information'
EQUALITY caseIgnoreMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )
# system schema
#attributetype ( 2.5.4.3 NAME ( 'cn' 'commonName' )
# DESC 'RFC2256: common name(s) for which the entity is known by'
# SUP name )
attributetype ( 2.5.4.4 NAME ( 'sn' 'surname' )
DESC 'RFC2256: last (family) name(s) for which the entity is known by'
SUP name )
attributetype ( 2.5.4.5 NAME 'serialNumber'
DESC 'RFC2256: serial number of the entity'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{64} )
# RFC 4519 definition ('countryName' in X.500 and RFC2256)
attributetype ( 2.5.4.6 NAME ( 'c' 'countryName' )
DESC 'RFC4519: two-letter ISO-3166 country code'
SUP name
SYNTAX 1.3.6.1.4.1.1466.115.121.1.11
SINGLE-VALUE )
#attributetype ( 2.5.4.6 NAME ( 'c' 'countryName' )
# DESC 'RFC2256: ISO-3166 country 2-letter code'
# SUP name SINGLE-VALUE )
attributetype ( 2.5.4.7 NAME ( 'l' 'localityName' )
DESC 'RFC2256: locality which this object resides in'
SUP name )
attributetype ( 2.5.4.8 NAME ( 'st' 'stateOrProvinceName' )
DESC 'RFC2256: state or province which this object resides in'
SUP name )
attributetype ( 2.5.4.9 NAME ( 'street' 'streetAddress' )
DESC 'RFC2256: street address of this object'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
attributetype ( 2.5.4.10 NAME ( 'o' 'organizationName' )
DESC 'RFC2256: organization this object belongs to'
SUP name )
attributetype ( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' )
DESC 'RFC2256: organizational unit this object belongs to'
SUP name )
attributetype ( 2.5.4.12 NAME 'title'
DESC 'RFC2256: title associated with the entity'
SUP name )
# system schema
#attributetype ( 2.5.4.13 NAME 'description'
# DESC 'RFC2256: descriptive information'
# EQUALITY caseIgnoreMatch
# SUBSTR caseIgnoreSubstringsMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )
# Deprecated by enhancedSearchGuide
attributetype ( 2.5.4.14 NAME 'searchGuide'
DESC 'RFC2256: search guide, deprecated by enhancedSearchGuide'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.25 )
attributetype ( 2.5.4.15 NAME 'businessCategory'
DESC 'RFC2256: business category'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
attributetype ( 2.5.4.16 NAME 'postalAddress'
DESC 'RFC2256: postal address'
EQUALITY caseIgnoreListMatch
SUBSTR caseIgnoreListSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )
attributetype ( 2.5.4.17 NAME 'postalCode'
DESC 'RFC2256: postal code'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{40} )
attributetype ( 2.5.4.18 NAME 'postOfficeBox'
DESC 'RFC2256: Post Office Box'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{40} )
attributetype ( 2.5.4.19 NAME 'physicalDeliveryOfficeName'
DESC 'RFC2256: Physical Delivery Office Name'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
attributetype ( 2.5.4.20 NAME 'telephoneNumber'
DESC 'RFC2256: Telephone Number'
EQUALITY telephoneNumberMatch
SUBSTR telephoneNumberSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.50{32} )
attributetype ( 2.5.4.21 NAME 'telexNumber'
DESC 'RFC2256: Telex Number'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.52 )
attributetype ( 2.5.4.22 NAME 'teletexTerminalIdentifier'
DESC 'RFC2256: Teletex Terminal Identifier'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.51 )
attributetype ( 2.5.4.23 NAME ( 'facsimileTelephoneNumber' 'fax' )
DESC 'RFC2256: Facsimile (Fax) Telephone Number'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.22 )
attributetype ( 2.5.4.24 NAME 'x121Address'
DESC 'RFC2256: X.121 Address'
EQUALITY numericStringMatch
SUBSTR numericStringSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{15} )
attributetype ( 2.5.4.25 NAME 'internationaliSDNNumber'
DESC 'RFC2256: international ISDN number'
EQUALITY numericStringMatch
SUBSTR numericStringSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{16} )
attributetype ( 2.5.4.26 NAME 'registeredAddress'
DESC 'RFC2256: registered postal address'
SUP postalAddress
SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )
attributetype ( 2.5.4.27 NAME 'destinationIndicator'
DESC 'RFC2256: destination indicator'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{128} )
attributetype ( 2.5.4.28 NAME 'preferredDeliveryMethod'
DESC 'RFC2256: preferred delivery method'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.14
SINGLE-VALUE )
attributetype ( 2.5.4.29 NAME 'presentationAddress'
DESC 'RFC2256: presentation address'
EQUALITY presentationAddressMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.43
SINGLE-VALUE )
attributetype ( 2.5.4.30 NAME 'supportedApplicationContext'
DESC 'RFC2256: supported application context'
EQUALITY objectIdentifierMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )
attributetype ( 2.5.4.31 NAME 'member'
DESC 'RFC2256: member of a group'
SUP distinguishedName )
attributetype ( 2.5.4.32 NAME 'owner'
DESC 'RFC2256: owner (of the object)'
SUP distinguishedName )
attributetype ( 2.5.4.33 NAME 'roleOccupant'
DESC 'RFC2256: occupant of role'
SUP distinguishedName )
# system schema
#attributetype ( 2.5.4.34 NAME 'seeAlso'
# DESC 'RFC2256: DN of related object'
# SUP distinguishedName )
# system schema
#attributetype ( 2.5.4.35 NAME 'userPassword'
# DESC 'RFC2256/2307: password of user'
# EQUALITY octetStringMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.40{128} )
# Must be transferred using ;binary
# with certificateExactMatch rule (per X.509)
attributetype ( 2.5.4.36 NAME 'userCertificate'
DESC 'RFC2256: X.509 user certificate, use ;binary'
EQUALITY certificateExactMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 )
# Must be transferred using ;binary
# with certificateExactMatch rule (per X.509)
attributetype ( 2.5.4.37 NAME 'cACertificate'
DESC 'RFC2256: X.509 CA certificate, use ;binary'
EQUALITY certificateExactMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 )
# Must be transferred using ;binary
attributetype ( 2.5.4.38 NAME 'authorityRevocationList'
DESC 'RFC2256: X.509 authority revocation list, use ;binary'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )
# Must be transferred using ;binary
attributetype ( 2.5.4.39 NAME 'certificateRevocationList'
DESC 'RFC2256: X.509 certificate revocation list, use ;binary'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )
# Must be stored and requested in the binary form
attributetype ( 2.5.4.40 NAME 'crossCertificatePair'
DESC 'RFC2256: X.509 cross certificate pair, use ;binary'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.10 )
# system schema
#attributetype ( 2.5.4.41 NAME 'name'
# EQUALITY caseIgnoreMatch
# SUBSTR caseIgnoreSubstringsMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )
attributetype ( 2.5.4.42 NAME ( 'givenName' 'gn' )
DESC 'RFC2256: first name(s) for which the entity is known by'
SUP name )
attributetype ( 2.5.4.43 NAME 'initials'
DESC 'RFC2256: initials of some or all of names, but not the surname(s).'
SUP name )
attributetype ( 2.5.4.44 NAME 'generationQualifier'
DESC 'RFC2256: name qualifier indicating a generation'
SUP name )
attributetype ( 2.5.4.45 NAME 'x500UniqueIdentifier'
DESC 'RFC2256: X.500 unique identifier'
EQUALITY bitStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )
attributetype ( 2.5.4.46 NAME 'dnQualifier'
DESC 'RFC2256: DN qualifier'
EQUALITY caseIgnoreMatch
ORDERING caseIgnoreOrderingMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )
attributetype ( 2.5.4.47 NAME 'enhancedSearchGuide'
DESC 'RFC2256: enhanced search guide'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.21 )
attributetype ( 2.5.4.48 NAME 'protocolInformation'
DESC 'RFC2256: protocol information'
EQUALITY protocolInformationMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.42 )
# system schema
#attributetype ( 2.5.4.49 NAME 'distinguishedName'
# EQUALITY distinguishedNameMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
attributetype ( 2.5.4.50 NAME 'uniqueMember'
DESC 'RFC2256: unique member of a group'
EQUALITY uniqueMemberMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )
attributetype ( 2.5.4.51 NAME 'houseIdentifier'
DESC 'RFC2256: house identifier'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )
# Must be transferred using ;binary
attributetype ( 2.5.4.52 NAME 'supportedAlgorithms'
DESC 'RFC2256: supported algorithms'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.49 )
# Must be transferred using ;binary
attributetype ( 2.5.4.53 NAME 'deltaRevocationList'
DESC 'RFC2256: delta revocation list; use ;binary'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )
attributetype ( 2.5.4.54 NAME 'dmdName'
DESC 'RFC2256: name of DMD'
SUP name )
attributetype ( 2.5.4.65 NAME 'pseudonym'
DESC 'X.520(4th): pseudonym for the object'
SUP name )
# Standard object classes from RFC2256
# system schema
#objectclass ( 2.5.6.0 NAME 'top'
# DESC 'RFC2256: top of the superclass chain'
# ABSTRACT
# MUST objectClass )
# system schema
#objectclass ( 2.5.6.1 NAME 'alias'
# DESC 'RFC2256: an alias'
# SUP top STRUCTURAL
# MUST aliasedObjectName )
objectclass ( 2.5.6.2 NAME 'country'
DESC 'RFC2256: a country'
SUP top STRUCTURAL
MUST c
MAY ( searchGuide $ description ) )
objectclass ( 2.5.6.3 NAME 'locality'
DESC 'RFC2256: a locality'
SUP top STRUCTURAL
MAY ( street $ seeAlso $ searchGuide $ st $ l $ description ) )
objectclass ( 2.5.6.4 NAME 'organization'
DESC 'RFC2256: an organization'
SUP top STRUCTURAL
MUST o
MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $
x121Address $ registeredAddress $ destinationIndicator $
preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $
telephoneNumber $ internationaliSDNNumber $
facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $
postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) )
objectclass ( 2.5.6.5 NAME 'organizationalUnit'
DESC 'RFC2256: an organizational unit'
SUP top STRUCTURAL
MUST ou
MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $
x121Address $ registeredAddress $ destinationIndicator $
preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $
telephoneNumber $ internationaliSDNNumber $
facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $
postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) )
objectclass ( 2.5.6.6 NAME 'person'
DESC 'RFC2256: a person'
SUP top STRUCTURAL
MUST ( sn $ cn )
MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) )
objectclass ( 2.5.6.7 NAME 'organizationalPerson'
DESC 'RFC2256: an organizational person'
SUP person STRUCTURAL
MAY ( title $ x121Address $ registeredAddress $ destinationIndicator $
preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $
telephoneNumber $ internationaliSDNNumber $
facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $
postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l ) )
objectclass ( 2.5.6.8 NAME 'organizationalRole'
DESC 'RFC2256: an organizational role'
SUP top STRUCTURAL
MUST cn
MAY ( x121Address $ registeredAddress $ destinationIndicator $
preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $
telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $
seeAlso $ roleOccupant $ preferredDeliveryMethod $ street $
postOfficeBox $ postalCode $ postalAddress $
physicalDeliveryOfficeName $ ou $ st $ l $ description ) )
objectclass ( 2.5.6.9 NAME 'groupOfNames'
DESC 'RFC2256: a group of names (DNs)'
SUP top STRUCTURAL
MUST ( member $ cn )
MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )
objectclass ( 2.5.6.10 NAME 'residentialPerson'
DESC 'RFC2256: an residential person'
SUP person STRUCTURAL
MUST l
MAY ( businessCategory $ x121Address $ registeredAddress $
destinationIndicator $ preferredDeliveryMethod $ telexNumber $
teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $
facsimileTelephoneNumber $ preferredDeliveryMethod $ street $
postOfficeBox $ postalCode $ postalAddress $
physicalDeliveryOfficeName $ st $ l ) )
objectclass ( 2.5.6.11 NAME 'applicationProcess'
DESC 'RFC2256: an application process'
SUP top STRUCTURAL
MUST cn
MAY ( seeAlso $ ou $ l $ description ) )
objectclass ( 2.5.6.12 NAME 'applicationEntity'
DESC 'RFC2256: an application entity'
SUP top STRUCTURAL
MUST ( presentationAddress $ cn )
MAY ( supportedApplicationContext $ seeAlso $ ou $ o $ l $
description ) )
objectclass ( 2.5.6.13 NAME 'dSA'
DESC 'RFC2256: a directory system agent (a server)'
SUP applicationEntity STRUCTURAL
MAY knowledgeInformation )
objectclass ( 2.5.6.14 NAME 'device'
DESC 'RFC2256: a device'
SUP top STRUCTURAL
MUST cn
MAY ( serialNumber $ seeAlso $ owner $ ou $ o $ l $ description ) )
objectclass ( 2.5.6.15 NAME 'strongAuthenticationUser'
DESC 'RFC2256: a strong authentication user'
SUP top AUXILIARY
MUST userCertificate )
objectclass ( 2.5.6.16 NAME 'certificationAuthority'
DESC 'RFC2256: a certificate authority'
SUP top AUXILIARY
MUST ( authorityRevocationList $ certificateRevocationList $
cACertificate ) MAY crossCertificatePair )
objectclass ( 2.5.6.17 NAME 'groupOfUniqueNames'
DESC 'RFC2256: a group of unique names (DN and Unique Identifier)'
SUP top STRUCTURAL
MUST ( uniqueMember $ cn )
MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )
objectclass ( 2.5.6.18 NAME 'userSecurityInformation'
DESC 'RFC2256: a user security information'
SUP top AUXILIARY
MAY ( supportedAlgorithms ) )
objectclass ( 2.5.6.16.2 NAME 'certificationAuthority-V2'
SUP certificationAuthority
AUXILIARY MAY ( deltaRevocationList ) )
objectclass ( 2.5.6.19 NAME 'cRLDistributionPoint'
SUP top STRUCTURAL
MUST ( cn )
MAY ( certificateRevocationList $ authorityRevocationList $
deltaRevocationList ) )
objectclass ( 2.5.6.20 NAME 'dmd'
SUP top STRUCTURAL
MUST ( dmdName )
MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $
x121Address $ registeredAddress $ destinationIndicator $
preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $
telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $
street $ postOfficeBox $ postalCode $ postalAddress $
physicalDeliveryOfficeName $ st $ l $ description ) )
#
# Object Classes from RFC 2587
#
objectclass ( 2.5.6.21 NAME 'pkiUser'
DESC 'RFC2587: a PKI user'
SUP top AUXILIARY
MAY userCertificate )
objectclass ( 2.5.6.22 NAME 'pkiCA'
DESC 'RFC2587: PKI certificate authority'
SUP top AUXILIARY
MAY ( authorityRevocationList $ certificateRevocationList $
cACertificate $ crossCertificatePair ) )
objectclass ( 2.5.6.23 NAME 'deltaCRL'
DESC 'RFC2587: PKI user'
SUP top AUXILIARY
MAY deltaRevocationList )
#
# Standard Track URI label schema from RFC 2079
# system schema
#attributetype ( 1.3.6.1.4.1.250.1.57 NAME 'labeledURI'
# DESC 'RFC2079: Uniform Resource Identifier with optional label'
# EQUALITY caseExactMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
objectclass ( 1.3.6.1.4.1.250.3.15 NAME 'labeledURIObject'
DESC 'RFC2079: object that contains the URI attribute type'
SUP top AUXILIARY
MAY ( labeledURI ) )
#
# Derived from RFC 1274, but with new "short names"
#
#attributetype ( 0.9.2342.19200300.100.1.1
# NAME ( 'uid' 'userid' )
# DESC 'RFC1274: user identifier'
# EQUALITY caseIgnoreMatch
# SUBSTR caseIgnoreSubstringsMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
attributetype ( 0.9.2342.19200300.100.1.3
NAME ( 'mail' 'rfc822Mailbox' )
DESC 'RFC1274: RFC822 Mailbox'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
objectclass ( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject'
DESC 'RFC1274: simple security object'
SUP top AUXILIARY
MUST userPassword )
# RFC 1274 + RFC 2247
attributetype ( 0.9.2342.19200300.100.1.25
NAME ( 'dc' 'domainComponent' )
DESC 'RFC1274/2247: domain component'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
# RFC 2247
objectclass ( 1.3.6.1.4.1.1466.344 NAME 'dcObject'
DESC 'RFC2247: domain component object'
SUP top AUXILIARY MUST dc )
# RFC 2377
objectclass ( 1.3.6.1.1.3.1 NAME 'uidObject'
DESC 'RFC2377: uid object'
SUP top AUXILIARY MUST uid )
# RFC 4524
# The 'associatedDomain' attribute specifies DNS [RFC1034][RFC2181]
# host names [RFC1123] that are associated with an object. That is,
# values of this attribute should conform to the following ABNF:
#
# domain = root / label *( DOT label )
# root = SPACE
# label = LETDIG [ *61( LETDIG / HYPHEN ) LETDIG ]
# LETDIG = %x30-39 / %x41-5A / %x61-7A ; "0" - "9" / "A"-"Z" / "a"-"z"
# SPACE = %x20 ; space (" ")
# HYPHEN = %x2D ; hyphen ("-")
# DOT = %x2E ; period (".")
attributetype ( 0.9.2342.19200300.100.1.37
NAME 'associatedDomain'
DESC 'RFC1274: domain associated with object'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
# RFC 2459 -- deprecated in favor of 'mail' (in cosine.schema)
attributetype ( 1.2.840.113549.1.9.1
NAME ( 'email' 'emailAddress' 'pkcs9email' )
DESC 'RFC3280: legacy attribute for email addresses in DNs'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} )

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,155 @@
# inetorgperson.schema -- InetOrgPerson (RFC2798)
# $OpenLDAP$
## This work is part of OpenLDAP Software <http://www.openldap.org/>.
##
## Copyright 1998-2019 The OpenLDAP Foundation.
## All rights reserved.
##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted only as authorized by the OpenLDAP
## Public License.
##
## A copy of this license is available in the file LICENSE in the
## top-level directory of the distribution or, alternatively, at
## <http://www.OpenLDAP.org/license.html>.
#
# InetOrgPerson (RFC2798)
#
# Depends upon
# Definition of an X.500 Attribute Type and an Object Class to Hold
# Uniform Resource Identifiers (URIs) [RFC2079]
# (core.schema)
#
# A Summary of the X.500(96) User Schema for use with LDAPv3 [RFC2256]
# (core.schema)
#
# The COSINE and Internet X.500 Schema [RFC1274] (cosine.schema)
# carLicense
# This multivalued field is used to record the values of the license or
# registration plate associated with an individual.
attributetype ( 2.16.840.1.113730.3.1.1
NAME 'carLicense'
DESC 'RFC2798: vehicle license or registration plate'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
# departmentNumber
# Code for department to which a person belongs. This can also be
# strictly numeric (e.g., 1234) or alphanumeric (e.g., ABC/123).
attributetype ( 2.16.840.1.113730.3.1.2
NAME 'departmentNumber'
DESC 'RFC2798: identifies a department within an organization'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
# displayName
# When displaying an entry, especially within a one-line summary list, it
# is useful to be able to identify a name to be used. Since other attri-
# bute types such as 'cn' are multivalued, an additional attribute type is
# needed. Display name is defined for this purpose.
attributetype ( 2.16.840.1.113730.3.1.241
NAME 'displayName'
DESC 'RFC2798: preferred name to be used when displaying entries'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
# employeeNumber
# Numeric or alphanumeric identifier assigned to a person, typically based
# on order of hire or association with an organization. Single valued.
attributetype ( 2.16.840.1.113730.3.1.3
NAME 'employeeNumber'
DESC 'RFC2798: numerically identifies an employee within an organization'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
# employeeType
# Used to identify the employer to employee relationship. Typical values
# used will be "Contractor", "Employee", "Intern", "Temp", "External", and
# "Unknown" but any value may be used.
attributetype ( 2.16.840.1.113730.3.1.4
NAME 'employeeType'
DESC 'RFC2798: type of employment for a person'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
# jpegPhoto
# Used to store one or more images of a person using the JPEG File
# Interchange Format [JFIF].
# Note that the jpegPhoto attribute type was defined for use in the
# Internet X.500 pilots but no referencable definition for it could be
# located.
attributetype ( 0.9.2342.19200300.100.1.60
NAME 'jpegPhoto'
DESC 'RFC2798: a JPEG image'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )
# preferredLanguage
# Used to indicate an individual's preferred written or spoken
# language. This is useful for international correspondence or human-
# computer interaction. Values for this attribute type MUST conform to
# the definition of the Accept-Language header field defined in
# [RFC2068] with one exception: the sequence "Accept-Language" ":"
# should be omitted. This is a single valued attribute type.
attributetype ( 2.16.840.1.113730.3.1.39
NAME 'preferredLanguage'
DESC 'RFC2798: preferred written or spoken language for a person'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
# userSMIMECertificate
# A PKCS#7 [RFC2315] SignedData, where the content that is signed is
# ignored by consumers of userSMIMECertificate values. It is
# recommended that values have a `contentType' of data with an absent
# `content' field. Values of this attribute contain a person's entire
# certificate chain and an smimeCapabilities field [RFC2633] that at a
# minimum describes their SMIME algorithm capabilities. Values for
# this attribute are to be stored and requested in binary form, as
# 'userSMIMECertificate;binary'. If available, this attribute is
# preferred over the userCertificate attribute for S/MIME applications.
## OpenLDAP note: ";binary" transfer should NOT be used as syntax is binary
attributetype ( 2.16.840.1.113730.3.1.40
NAME 'userSMIMECertificate'
DESC 'RFC2798: PKCS#7 SignedData used to support S/MIME'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 )
# userPKCS12
# PKCS #12 [PKCS12] provides a format for exchange of personal identity
# information. When such information is stored in a directory service,
# the userPKCS12 attribute should be used. This attribute is to be stored
# and requested in binary form, as 'userPKCS12;binary'. The attribute
# values are PFX PDUs stored as binary data.
## OpenLDAP note: ";binary" transfer should NOT be used as syntax is binary
attributetype ( 2.16.840.1.113730.3.1.216
NAME 'userPKCS12'
DESC 'RFC2798: personal identity information, a PKCS #12 PFX'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 )
# inetOrgPerson
# The inetOrgPerson represents people who are associated with an
# organization in some way. It is a structural class and is derived
# from the organizationalPerson which is defined in X.521 [X521].
objectclass ( 2.16.840.1.113730.3.2.2
NAME 'inetOrgPerson'
DESC 'RFC2798: Internet Organizational Person'
SUP organizationalPerson
STRUCTURAL
MAY (
audio $ businessCategory $ carLicense $ departmentNumber $
displayName $ employeeNumber $ employeeType $ givenName $
homePhone $ homePostalAddress $ initials $ jpegPhoto $
labeledURI $ mail $ manager $ mobile $ o $ pager $
photo $ roomNumber $ secretary $ uid $ userCertificate $
x500uniqueIdentifier $ preferredLanguage $
userSMIMECertificate $ userPKCS12 )
)

View file

@ -0,0 +1,88 @@
## LDAP Schema Yunohost EMAIL
## Version 0.1
## Adrien Beudin
# Attributes
attributetype ( 1.3.6.1.4.1.40328.1.20.2.1
NAME 'maildrop'
DESC 'Mail addresses where mails are forwarded -- ie forwards'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512})
attributetype ( 1.3.6.1.4.1.40328.1.20.2.2
NAME 'mailalias'
DESC 'Mail addresses accepted by this account -- ie aliases'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512})
attributetype ( 1.3.6.1.4.1.40328.1.20.2.3
NAME 'mailenable'
DESC 'Mail Account validity'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{8})
attributetype ( 1.3.6.1.4.1.40328.1.20.2.4
NAME 'mailbox'
DESC 'Mailbox path where mails are delivered'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512})
attributetype ( 1.3.6.1.4.1.40328.1.20.2.5
NAME 'virtualdomain'
DESC 'A mail domain name'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512})
attributetype ( 1.3.6.1.4.1.40328.1.20.2.6
NAME 'virtualdomaindescription'
DESC 'Virtual domain description'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512})
attributetype ( 1.3.6.1.4.1.40328.1.20.2.7
NAME 'mailuserquota'
DESC 'Mailbox quota for a user'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{16} SINGLE-VALUE )
# Mail Account Objectclass
objectclass ( 1.3.6.1.4.1.40328.1.1.2.1
NAME 'mailAccount'
DESC 'Mail Account'
SUP top
AUXILIARY
MUST (
mail
)
MAY (
mailalias $ maildrop $ mailenable $ mailbox $ mailuserquota
)
)
# Mail Domain Objectclass
objectclass ( 1.3.6.1.4.1.40328.1.1.2.2
NAME 'mailDomain'
DESC 'Domain mail entry'
SUP top
STRUCTURAL
MUST (
virtualdomain
)
MAY (
virtualdomaindescription $ mailuserquota
)
)
# Mail Group Objectclass
objectclass ( 1.3.6.1.4.1.40328.1.1.2.3
NAME 'mailGroup' SUP top AUXILIARY
DESC 'Mail Group'
MUST ( mail )
)

View file

@ -0,0 +1,237 @@
# $OpenLDAP$
## This work is part of OpenLDAP Software <http://www.openldap.org/>.
##
## Copyright 1998-2019 The OpenLDAP Foundation.
## All rights reserved.
##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted only as authorized by the OpenLDAP
## Public License.
##
## A copy of this license is available in the file LICENSE in the
## top-level directory of the distribution or, alternatively, at
## <http://www.OpenLDAP.org/license.html>.
# Definitions from RFC2307 (Experimental)
# An Approach for Using LDAP as a Network Information Service
# Depends upon core.schema and cosine.schema
# Note: The definitions in RFC2307 are given in syntaxes closely related
# to those in RFC2252, however, some liberties are taken that are not
# supported by RFC2252. This file has been written following RFC2252
# strictly.
# OID Base is iso(1) org(3) dod(6) internet(1) directory(1) nisSchema(1).
# i.e. nisSchema in RFC2307 is 1.3.6.1.1.1
#
# Syntaxes are under 1.3.6.1.1.1.0 (two new syntaxes are defined)
# validaters for these syntaxes are incomplete, they only
# implement printable string validation (which is good as the
# common use of these syntaxes violates the specification).
# Attribute types are under 1.3.6.1.1.1.1
# Object classes are under 1.3.6.1.1.1.2
# Attribute Type Definitions
# builtin
#attributetype ( 1.3.6.1.1.1.1.0 NAME 'uidNumber'
# DESC 'An integer uniquely identifying a user in an administrative domain'
# EQUALITY integerMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
# builtin
#attributetype ( 1.3.6.1.1.1.1.1 NAME 'gidNumber'
# DESC 'An integer uniquely identifying a group in an administrative domain'
# EQUALITY integerMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.2 NAME 'gecos'
DESC 'The GECOS field; the common name'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.3 NAME 'homeDirectory'
DESC 'The absolute path to the home directory'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.4 NAME 'loginShell'
DESC 'The path to the login shell'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.6 NAME 'shadowMin'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.7 NAME 'shadowMax'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.8 NAME 'shadowWarning'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.9 NAME 'shadowInactive'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.10 NAME 'shadowExpire'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.11 NAME 'shadowFlag'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.12 NAME 'memberUid'
EQUALITY caseExactIA5Match
SUBSTR caseExactIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup'
EQUALITY caseExactIA5Match
SUBSTR caseExactIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple'
DESC 'Netgroup triple'
SYNTAX 1.3.6.1.1.1.0.0 )
attributetype ( 1.3.6.1.1.1.1.15 NAME 'ipServicePort'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol'
SUP name )
attributetype ( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber'
DESC 'IP address'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} )
attributetype ( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber'
DESC 'IP network'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber'
DESC 'IP netmask'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.22 NAME 'macAddress'
DESC 'MAC address'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} )
attributetype ( 1.3.6.1.1.1.1.23 NAME 'bootParameter'
DESC 'rpc.bootparamd parameter'
SYNTAX 1.3.6.1.1.1.0.1 )
attributetype ( 1.3.6.1.1.1.1.24 NAME 'bootFile'
DESC 'Boot image name'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.26 NAME 'nisMapName'
SUP name )
attributetype ( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry'
EQUALITY caseExactIA5Match
SUBSTR caseExactIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{1024} SINGLE-VALUE )
# Object Class Definitions
objectclass ( 1.3.6.1.1.1.2.0 NAME 'posixAccount'
DESC 'Abstraction of an account with POSIX attributes'
SUP top AUXILIARY
MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
MAY ( userPassword $ loginShell $ gecos $ description ) )
objectclass ( 1.3.6.1.1.1.2.1 NAME 'shadowAccount'
DESC 'Additional attributes for shadow passwords'
SUP top AUXILIARY
MUST uid
MAY ( userPassword $ shadowLastChange $ shadowMin $
shadowMax $ shadowWarning $ shadowInactive $
shadowExpire $ shadowFlag $ description ) )
objectclass ( 1.3.6.1.1.1.2.2 NAME 'posixGroup'
DESC 'Abstraction of a group of accounts'
SUP top STRUCTURAL
MUST ( cn $ gidNumber )
MAY ( userPassword $ memberUid $ description ) )
objectclass ( 1.3.6.1.1.1.2.3 NAME 'ipService'
DESC 'Abstraction an Internet Protocol service'
SUP top STRUCTURAL
MUST ( cn $ ipServicePort $ ipServiceProtocol )
MAY ( description ) )
objectclass ( 1.3.6.1.1.1.2.4 NAME 'ipProtocol'
DESC 'Abstraction of an IP protocol'
SUP top STRUCTURAL
MUST ( cn $ ipProtocolNumber $ description )
MAY description )
objectclass ( 1.3.6.1.1.1.2.5 NAME 'oncRpc'
DESC 'Abstraction of an ONC/RPC binding'
SUP top STRUCTURAL
MUST ( cn $ oncRpcNumber $ description )
MAY description )
objectclass ( 1.3.6.1.1.1.2.6 NAME 'ipHost'
DESC 'Abstraction of a host, an IP device'
SUP top AUXILIARY
MUST ( cn $ ipHostNumber )
MAY ( l $ description $ manager ) )
objectclass ( 1.3.6.1.1.1.2.7 NAME 'ipNetwork'
DESC 'Abstraction of an IP network'
SUP top STRUCTURAL
MUST ( cn $ ipNetworkNumber )
MAY ( ipNetmaskNumber $ l $ description $ manager ) )
objectclass ( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup'
DESC 'Abstraction of a netgroup'
SUP top STRUCTURAL
MUST cn
MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) )
objectclass ( 1.3.6.1.1.1.2.9 NAME 'nisMap'
DESC 'A generic abstraction of a NIS map'
SUP top STRUCTURAL
MUST nisMapName
MAY description )
objectclass ( 1.3.6.1.1.1.2.10 NAME 'nisObject'
DESC 'An entry in a NIS map'
SUP top STRUCTURAL
MUST ( cn $ nisMapEntry $ nisMapName )
MAY description )
objectclass ( 1.3.6.1.1.1.2.11 NAME 'ieee802Device'
DESC 'A device with a MAC address'
SUP top AUXILIARY
MAY macAddress )
objectclass ( 1.3.6.1.1.1.2.12 NAME 'bootableDevice'
DESC 'A device with boot parameters'
SUP top AUXILIARY
MAY ( bootFile $ bootParameter ) )

View file

@ -0,0 +1,76 @@
#
# OpenLDAP schema file for Sudo
# Save as /etc/openldap/schema/sudo.schema
#
attributetype ( 1.3.6.1.4.1.15953.9.1.1
NAME 'sudoUser'
DESC 'User(s) who may run sudo'
EQUALITY caseExactIA5Match
SUBSTR caseExactIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.4.1.15953.9.1.2
NAME 'sudoHost'
DESC 'Host(s) who may run sudo'
EQUALITY caseExactIA5Match
SUBSTR caseExactIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.4.1.15953.9.1.3
NAME 'sudoCommand'
DESC 'Command(s) to be executed by sudo'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.4.1.15953.9.1.4
NAME 'sudoRunAs'
DESC 'User(s) impersonated by sudo (deprecated)'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.4.1.15953.9.1.5
NAME 'sudoOption'
DESC 'Options(s) followed by sudo'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.4.1.15953.9.1.6
NAME 'sudoRunAsUser'
DESC 'User(s) impersonated by sudo'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.4.1.15953.9.1.7
NAME 'sudoRunAsGroup'
DESC 'Group(s) impersonated by sudo'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.4.1.15953.9.1.8
NAME 'sudoNotBefore'
DESC 'Start of time interval for which the entry is valid'
EQUALITY generalizedTimeMatch
ORDERING generalizedTimeOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )
attributetype ( 1.3.6.1.4.1.15953.9.1.9
NAME 'sudoNotAfter'
DESC 'End of time interval for which the entry is valid'
EQUALITY generalizedTimeMatch
ORDERING generalizedTimeOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )
attributeTypes ( 1.3.6.1.4.1.15953.9.1.10
NAME 'sudoOrder'
DESC 'an integer to order the sudoRole entries'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )
objectclass ( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' SUP top STRUCTURAL
DESC 'Sudoer Entries'
MUST ( cn )
MAY ( sudoUser $ sudoHost $ sudoCommand $ sudoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ sudoOrder $ sudoNotBefore $ sudoNotAfter $
description )
)

View file

@ -0,0 +1,33 @@
#dn: cn=yunohost,cn=schema,cn=config
#objectClass: olcSchemaConfig
#cn: yunohost
# ATTRIBUTES
# For Permission
attributetype ( 1.3.6.1.4.1.17953.9.1.1 NAME 'permission'
DESC 'Yunohost permission on user and group side'
SUP distinguishedName )
attributetype ( 1.3.6.1.4.1.17953.9.1.2 NAME 'groupPermission'
DESC 'Yunohost permission for a group on permission side'
SUP distinguishedName )
attributetype ( 1.3.6.1.4.1.17953.9.1.3 NAME 'inheritPermission'
DESC 'Yunohost permission for user on permission side'
SUP distinguishedName )
attributetype ( 1.3.6.1.4.1.17953.9.1.4 NAME 'URL'
DESC 'Yunohost application URL'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
# OBJECTCLASS
# For Applications
objectclass ( 1.3.6.1.4.1.17953.9.2.1 NAME 'groupOfNamesYnh'
DESC 'Yunohost user group'
SUP top AUXILIARY
MAY ( member $ businessCategory $ seeAlso $ owner $ ou $ o $ permission ) )
objectclass ( 1.3.6.1.4.1.17953.9.2.2 NAME 'permissionYnh'
DESC 'a Yunohost application'
SUP top AUXILIARY
MUST cn
MAY ( groupPermission $ inheritPermission $ URL ) )
# For User
objectclass ( 1.3.6.1.4.1.17953.9.2.3 NAME 'userPermissionYnh'
DESC 'a Yunohost application'
SUP top AUXILIARY
MAY ( permission ) )

View file

@ -0,0 +1,94 @@
serverID %(serverid)s
moduleload back_%(database)s
moduleload memberof
%(include_directives)s
loglevel %(loglevel)s
#allow bind_v2
database %(database)s
directory "%(directory)s"
suffix "%(suffix)s"
rootdn "%(rootdn)s"
rootpw "%(rootpw)s"
TLSCACertificateFile "%(cafile)s"
TLSCertificateFile "%(servercert)s"
TLSCertificateKeyFile "%(serverkey)s"
authz-regexp
"gidnumber=%(root_gid)s\\+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth"
"%(rootdn)s"
index objectClass eq
index uid,sudoUser eq,sub
index entryCSN,entryUUID eq
index cn,mail eq
index gidNumber,uidNumber eq
index member,memberUid,uniqueMember eq
index virtualdomain eq
# The userPassword by default can be changed
# by the entry owning it if they are authenticated.
# Others should not be able to see it, except the
# admin entry below
# These access lines apply to database #1 only
access to attrs=userPassword,shadowLastChange
by dn="cn=admin,dc=yunohost,dc=org" write
by dn.exact="gidNumber=%(root_gid)s+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth" write
by anonymous auth
by self write
by * none
# Personnal information can be changed by the entry
# owning it if they are authenticated.
# Others should be able to see it.
access to attrs=cn,gecos,givenName,mail,maildrop,displayName,sn
by dn="cn=admin,dc=yunohost,dc=org" write
by dn.exact="gidNumber=%(root_gid)s+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth" write
by self write
by * read
# Ensure read access to the base for things like
# supportedSASLMechanisms. Without this you may
# have problems with SASL not knowing what
# mechanisms are available and the like.
# Note that this is covered by the 'access to *'
# ACL below too but if you change that as people
# are wont to do you'll still need this if you
# want SASL (and possible ldap_files things) to work
# happily.
access to dn.base="" by * read
# The admin dn has full write access, everyone else
# can read everything.
access to *
by dn="cn=admin,dc=yunohost,dc=org" write
by dn.exact="gidNumber=%(root_gid)s+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth" write
by group/groupOfNames/Member="cn=admin,ou=groups,dc=yunohost,dc=org" write
by * read
# Configure Memberof Overlay (used for Yunohost permission)
# Link user <-> group
#dn: olcOverlay={0}memberof,olcDatabase={1}mdb,cn=config
overlay memberof
memberof-group-oc groupOfNamesYnh
memberof-member-ad member
memberof-memberof-ad memberOf
memberof-dangling error
memberof-refint TRUE
# Link permission <-> groupes
#dn: olcOverlay={1}memberof,olcDatabase={1}mdb,cn=config
overlay memberof
memberof-group-oc permissionYnh
memberof-member-ad groupPermission
memberof-memberof-ad permission
memberof-dangling error
memberof-refint TRUE
# Link permission <-> user
#dn: olcOverlay={2}memberof,olcDatabase={1}mdb,cn=config
overlay memberof
memberof-group-oc permissionYnh
memberof-member-ad inheritPermission
memberof-memberof-ad permission
memberof-dangling error
memberof-refint TRUE

205
test/ldap_files/tests.ldif Normal file
View file

@ -0,0 +1,205 @@
dn: dc=yunohost,dc=org
dc: yunohost
o: yunohost.org
objectclass: top
objectclass: dcObject
objectclass: organization
dn: cn=admin,dc=yunohost,dc=org
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: admin
userPassword: yunohost
#dn: ou=people,dc=yunohost,dc=org
#objectClass: organizationalUnit
#ou: people
#
#dn: ou=moregroups,dc=yunohost,dc=org
#objectClass: organizationalUnit
#ou: moregroups
#
#dn: ou=mirror_groups,dc=yunohost,dc=org
#objectClass: organizationalUnit
#ou: mirror_groups
#
#
#dn: uid=alice,ou=people,dc=yunohost,dc=org
#objectClass: person
#objectClass: organizationalPerson
#objectClass: inetOrgPerson
#objectClass: posixAccount
#cn: alice
#uid: alice
#userPassword: password
#uidNumber: 1000
#gidNumber: 1000
#givenName: Alice
#sn: Adams
#homeDirectory: /home/alice
#
#dn: uid=bob,ou=people,dc=yunohost,dc=org
#objectClass: person
#objectClass: organizationalPerson
#objectClass: inetOrgPerson
#objectClass: posixAccount
#cn: bob
#uid: bob
#userPassword: password
#uidNumber: 1001
#gidNumber: 50
#givenName: Robert
#sn: Barker
#homeDirectory: /home/bob
#
#dn: uid=dreßler,ou=people,dc=yunohost,dc=org
#objectClass: person
#objectClass: organizationalPerson
#objectClass: inetOrgPerson
#objectClass: posixAccount
#cn: dreßler
#uid: dreßler
#userPassword: password
#uidNumber: 1002
#gidNumber: 50
#givenName: Wolfgang
#sn: Dreßler
#homeDirectory: /home/dressler
#
#dn: uid=nobody,ou=people,dc=yunohost,dc=org
#objectClass: person
#objectClass: organizationalPerson
#objectClass: inetOrgPerson
#objectClass: posixAccount
#cn: nobody
#uid: nobody
#userPassword: password
#uidNumber: 1003
#gidNumber: 50
#sn: nobody
#homeDirectory: /home/nobody
#
#dn: uid=nonposix,ou=people,dc=yunohost,dc=org
#objectClass: person
#objectClass: organizationalPerson
#objectClass: inetOrgPerson
#cn: nonposix
#uid: nonposix
#userPassword: password
#sn: nonposix
#
#
## posixGroup objects
#dn: cn=active_px,ou=moregroups,dc=yunohost,dc=org
#objectClass: posixGroup
#cn: active_px
#gidNumber: 1000
#memberUid: nonposix
#
#dn: cn=staff_px,ou=moregroups,dc=yunohost,dc=org
#objectClass: posixGroup
#cn: staff_px
#gidNumber: 1001
#memberUid: alice
#memberUid: nonposix
#
#dn: cn=superuser_px,ou=moregroups,dc=yunohost,dc=org
#objectClass: posixGroup
#cn: superuser_px
#gidNumber: 1002
#memberUid: alice
#memberUid: nonposix
#
#
## groupOfNames groups
#dn: cn=empty_gon,ou=moregroups,dc=yunohost,dc=org
#cn: empty_gon
#objectClass: groupOfNames
#member:
#
#dn: cn=active_gon,ou=moregroups,dc=yunohost,dc=org
#cn: active_gon
#objectClass: groupOfNames
#member: uid=alice,ou=people,dc=yunohost,dc=org
#
#dn: cn=staff_gon,ou=moregroups,dc=yunohost,dc=org
#cn: staff_gon
#objectClass: groupOfNames
#member: uid=alice,ou=people,dc=yunohost,dc=org
#
#dn: cn=superuser_gon,ou=moregroups,dc=yunohost,dc=org
#cn: superuser_gon
#objectClass: groupOfNames
#member: uid=alice,ou=people,dc=yunohost,dc=org
#
#dn: cn=other_gon,ou=moregroups,dc=yunohost,dc=org
#cn: other_gon
#objectClass: groupOfNames
#member: uid=bob,ou=people,dc=yunohost,dc=org
#
#
## groupOfNames objects for LDAPGroupQuery testing
#dn: ou=query_groups,dc=yunohost,dc=org
#objectClass: organizationalUnit
#ou: query_groups
#
#dn: cn=alice_gon,ou=query_groups,dc=yunohost,dc=org
#cn: alice_gon
#objectClass: groupOfNames
#member: uid=alice,ou=people,dc=yunohost,dc=org
#
#dn: cn=mutual_gon,ou=query_groups,dc=yunohost,dc=org
#cn: mutual_gon
#objectClass: groupOfNames
#member: uid=alice,ou=people,dc=yunohost,dc=org
#member: uid=bob,ou=people,dc=yunohost,dc=org
#
#dn: cn=bob_gon,ou=query_groups,dc=yunohost,dc=org
#cn: bob_gon
#objectClass: groupOfNames
#member: uid=bob,ou=people,dc=yunohost,dc=org
#
#dn: cn=dreßler_gon,ou=query_groups,dc=yunohost,dc=org
#cn: dreßler_gon
#objectClass: groupOfNames
#member: uid=dreßler,ou=people,dc=yunohost,dc=org
#
#
## groupOfNames objects for selective group mirroring.
#dn: cn=mirror1,ou=mirror_groups,dc=yunohost,dc=org
#cn: mirror1
#objectClass: groupOfNames
#member: uid=alice,ou=people,dc=yunohost,dc=org
#
#dn: cn=mirror2,ou=mirror_groups,dc=yunohost,dc=org
#cn: mirror2
#objectClass: groupOfNames
#member:
#
#dn: cn=mirror3,ou=mirror_groups,dc=yunohost,dc=org
#cn: mirror3
#objectClass: groupOfNames
#member: uid=alice,ou=people,dc=yunohost,dc=org
#
#dn: cn=mirror4,ou=mirror_groups,dc=yunohost,dc=org
#cn: mirror4
#objectClass: groupOfNames
#member:
#
#
## Nested groups with a circular reference
#dn: cn=parent_gon,ou=moregroups,dc=yunohost,dc=org
#cn: parent_gon
#objectClass: groupOfNames
#member: cn=nested_gon,ou=moregroups,dc=yunohost,dc=org
#
#dn: CN=nested_gon,ou=moregroups,dc=yunohost,dc=org
#cn: nested_gon
#objectClass: groupOfNames
#member: uid=alice,ou=people,dc=yunohost,dc=org
#member: cn=circular_gon,ou=moregroups,dc=yunohost,dc=org
#
#dn: cn=circular_gon,ou=moregroups,dc=yunohost,dc=org
#cn: circular_gon
#objectClass: groupOfNames
#member: cn=parent_gon,ou=moregroups,dc=yunohost,dc=org

View file

@ -1,3 +1,5 @@
{
"foo": "bar"
"foo": "bar",
"Dummy Password": "Dummy Password",
"Dummy Yoloswag Password": "Dummy Yoloswag Password"
}

104
test/src/ldap_server.py Normal file
View file

@ -0,0 +1,104 @@
try:
import slapdtest
except ImportError:
import old_slapdtest as slapdtest
import os
from moulinette.authenticators import ldap as m_ldap
HERE = os.path.abspath(os.path.dirname(__file__))
class LDAPServer:
def __init__(self):
self.server_default = slapdtest.SlapdObject()
with open(os.path.join(HERE, "..", "ldap_files", "slapd.conf.template")) as f:
SLAPD_CONF_TEMPLATE = f.read()
self.server_default.slapd_conf_template = SLAPD_CONF_TEMPLATE
self.server_default.suffix = "dc=yunohost,dc=org"
self.server_default.root_cn = "admin"
self.server_default.SCHEMADIR = os.path.join(HERE, "..", "ldap_files", "schema")
self.server_default.openldap_schema_files = [
"core.schema",
"cosine.schema",
"nis.schema",
"inetorgperson.schema",
"sudo.schema",
"yunohost.schema",
"mailserver.schema",
]
self.server = None
self.uri = ""
def start(self):
self.server = self.server_default
self.server.start()
self.uri = self.server.ldapi_uri
with open(os.path.join(HERE, "..", "ldap_files", "tests.ldif")) as fp:
ldif = fp.read().decode("utf-8")
self.server.ldapadd(ldif)
self.tools_ldapinit()
def stop(self):
if self.server:
self.server.stop()
def __del__(self):
if self.server:
self.server.stop()
def tools_ldapinit(self):
"""
YunoHost LDAP initialization
"""
import yaml
with open(os.path.join(HERE, "..", "ldap_files", "ldap_scheme.yml")) as f:
ldap_map = yaml.load(f)
def _get_ldap_interface():
conf = {
"vendor": "ldap",
"name": "as-root",
"parameters": {
"uri": self.server.ldapi_uri,
"base_dn": "dc=yunohost,dc=org",
"user_rdn": "gidNumber=%s+uidNumber=%s,cn=peercred,cn=external,cn=auth"
% (os.getgid(), os.getuid()),
},
"extra": {},
}
_ldap_interface = m_ldap.Authenticator(**conf)
return _ldap_interface
ldap_interface = _get_ldap_interface()
for rdn, attr_dict in ldap_map["parents"].items():
ldap_interface.add(rdn, attr_dict)
for rdn, attr_dict in ldap_map["children"].items():
ldap_interface.add(rdn, attr_dict)
for rdn, attr_dict in ldap_map["depends_children"].items():
ldap_interface.add(rdn, attr_dict)
admin_dict = {
"cn": ["admin"],
"uid": ["admin"],
"description": ["LDAP Administrator"],
"gidNumber": ["1007"],
"uidNumber": ["1007"],
"homeDirectory": ["/home/admin"],
"loginShell": ["/bin/bash"],
"objectClass": [
"organizationalRole",
"posixAccount",
"simpleSecurityObject",
],
"userPassword": ["yunohost"],
}
ldap_interface.update("cn=admin", admin_dict)

View file

@ -0,0 +1,2 @@
I have adapted the code from https://github.com/python-ldap/python-ldap/tree/master/Lib/slapdtest since the version of python-ldap we use does not provide the import slapdtest.
This part will must be removed once we switch to python3

View file

@ -0,0 +1,2 @@
# flake8: noqa
from ._slapdtest import SlapdObject

View file

@ -0,0 +1,653 @@
# -*- coding: utf-8 -*-
# flake8: noqa
"""
slapdtest - module for spawning test instances of OpenLDAP's slapd server
See https://www.python-ldap.org/ for details.
"""
from __future__ import unicode_literals
import os
import socket
import sys
import time
import subprocess
import logging
import atexit
from logging.handlers import SysLogHandler
import unittest
# Switch off processing .ldaprc or ldap.conf before importing _ldap
os.environ["LDAPNOINIT"] = "1"
import ldap
from urllib import quote_plus
try:
from shutil import which
except ImportError:
# shutil.which() from Python 3.6
# "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
# 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation;
# All Rights Reserved"
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Given a command, mode, and a PATH string, return the path which
conforms to the given mode on the PATH, or None if there is no such
file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
of os.environ.get("PATH"), or can be overridden with a custom search
path.
"""
# Check that a given file can be accessed with the correct mode.
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
return os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn)
# If we're given a path with a directory part, look it up directly rather
# than referring to PATH directories. This includes checking relative to the
# current directory, e.g. ./script
if os.path.dirname(cmd):
if _access_check(cmd, mode):
return cmd
return None
if path is None:
path = os.environ.get("PATH", os.defpath)
if not path:
return None
path = path.split(os.pathsep)
if sys.platform == "win32":
# The current directory takes precedence on Windows.
if not os.curdir in path:
path.insert(0, os.curdir)
# PATHEXT is necessary to check on Windows.
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
# See if the given file matches any of the expected path extensions.
# This will allow us to short circuit when given "python.exe".
# If it does match, only test that one, otherwise we have to try
# others.
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
files = [cmd]
else:
files = [cmd + ext for ext in pathext]
else:
# On other platforms you don't have things like PATHEXT to tell you
# what file suffixes are executable, so just pass on cmd as-is.
files = [cmd]
seen = set()
for dir in path:
normdir = os.path.normcase(dir)
if not normdir in seen:
seen.add(normdir)
for thefile in files:
name = os.path.join(dir, thefile)
if _access_check(name, mode):
return name
return None
HERE = os.path.abspath(os.path.dirname(__file__))
# a template string for generating simple slapd.conf file
SLAPD_CONF_TEMPLATE = r"""
serverID %(serverid)s
moduleload back_%(database)s
%(include_directives)s
loglevel %(loglevel)s
allow bind_v2
authz-regexp
"gidnumber=%(root_gid)s\\+uidnumber=%(root_uid)s,cn=peercred,cn=external,cn=auth"
"%(rootdn)s"
database %(database)s
directory "%(directory)s"
suffix "%(suffix)s"
rootdn "%(rootdn)s"
rootpw "%(rootpw)s"
TLSCACertificateFile "%(cafile)s"
TLSCertificateFile "%(servercert)s"
TLSCertificateKeyFile "%(serverkey)s"
# ignore missing client cert but fail with invalid client cert
TLSVerifyClient try
authz-regexp
"C=DE, O=python-ldap, OU=slapd-test, CN=([A-Za-z]+)"
"ldap://ou=people,dc=local???($1)"
"""
LOCALHOST = "127.0.0.1"
CI_DISABLED = set(os.environ.get("CI_DISABLED", "").split(":"))
if "LDAPI" in CI_DISABLED:
HAVE_LDAPI = False
else:
HAVE_LDAPI = hasattr(socket, "AF_UNIX")
def identity(test_item):
"""Identity decorator
"""
return test_item
def skip_unless_ci(reason, feature=None):
"""Skip test unless test case is executed on CI like Travis CI
"""
if not os.environ.get("CI", False):
return unittest.skip(reason)
elif feature in CI_DISABLED:
return unittest.skip(reason)
else:
# Don't skip on Travis
return identity
def requires_tls():
"""Decorator for TLS tests
Tests are not skipped on CI (e.g. Travis CI)
"""
if not ldap.TLS_AVAIL:
return skip_unless_ci("test needs ldap.TLS_AVAIL", feature="TLS")
else:
return identity
def requires_sasl():
if not ldap.SASL_AVAIL:
return skip_unless_ci("test needs ldap.SASL_AVAIL", feature="SASL")
else:
return identity
def requires_ldapi():
if not HAVE_LDAPI:
return skip_unless_ci("test needs ldapi support (AF_UNIX)", feature="LDAPI")
else:
return identity
def _add_sbin(path):
"""Add /sbin and related directories to a command search path"""
directories = path.split(os.pathsep)
if sys.platform != "win32":
for sbin in "/usr/local/sbin", "/sbin", "/usr/sbin":
if sbin not in directories:
directories.append(sbin)
return os.pathsep.join(directories)
def combined_logger(
log_name,
log_level=logging.WARN,
sys_log_format="%(levelname)s %(message)s",
console_log_format="%(asctime)s %(levelname)s %(message)s",
):
"""
Returns a combined SysLogHandler/StreamHandler logging instance
with formatters
"""
if "LOGLEVEL" in os.environ:
log_level = os.environ["LOGLEVEL"]
try:
log_level = int(log_level)
except ValueError:
pass
# for writing to syslog
new_logger = logging.getLogger(log_name)
if sys_log_format and os.path.exists("/dev/log"):
my_syslog_formatter = logging.Formatter(
fmt=" ".join((log_name, sys_log_format))
)
my_syslog_handler = logging.handlers.SysLogHandler(
address="/dev/log", facility=SysLogHandler.LOG_DAEMON,
)
my_syslog_handler.setFormatter(my_syslog_formatter)
new_logger.addHandler(my_syslog_handler)
if console_log_format:
my_stream_formatter = logging.Formatter(fmt=console_log_format)
my_stream_handler = logging.StreamHandler()
my_stream_handler.setFormatter(my_stream_formatter)
new_logger.addHandler(my_stream_handler)
new_logger.setLevel(log_level)
return new_logger # end of combined_logger()
class SlapdObject(object):
"""
Controller class for a slapd instance, OpenLDAP's server.
This class creates a temporary data store for slapd, runs it
listening on a private Unix domain socket and TCP port,
and initializes it with a top-level entry and the root user.
When a reference to an instance of this class is lost, the slapd
server is shut down.
An instance can be used as a context manager. When exiting the context
manager, the slapd server is shut down and the temporary data store is
removed.
.. versionchanged:: 3.1
Added context manager functionality
"""
slapd_conf_template = SLAPD_CONF_TEMPLATE
database = "mdb"
suffix = "dc=slapd-test,dc=python-ldap,dc=org"
root_cn = "Manager"
root_pw = "password"
slapd_loglevel = "stats stats2"
local_host = "127.0.0.1"
testrunsubdirs = ("schema",)
openldap_schema_files = ("core.schema",)
TMPDIR = os.environ.get("TMP", os.getcwd())
if "SCHEMA" in os.environ:
SCHEMADIR = os.environ["SCHEMA"]
elif os.path.isdir("/etc/openldap/schema"):
SCHEMADIR = "/etc/openldap/schema"
elif os.path.isdir("/etc/ldap/schema"):
SCHEMADIR = "/etc/ldap/schema"
else:
SCHEMADIR = None
BIN_PATH = os.environ.get("BIN", os.environ.get("PATH", os.defpath))
SBIN_PATH = os.environ.get("SBIN", _add_sbin(BIN_PATH))
# time in secs to wait before trying to access slapd via LDAP (again)
_start_sleep = 1.5
# create loggers once, multiple calls mess up refleak tests
_log = combined_logger("python-ldap-test")
def __init__(self):
self._proc = None
self._port = self._avail_tcp_port()
self.server_id = self._port % 4096
self.testrundir = os.path.join(self.TMPDIR, "python-ldap-test-%d" % self._port)
self._schema_prefix = os.path.join(self.testrundir, "schema")
self._slapd_conf = os.path.join(self.testrundir, "slapd.conf")
self._db_directory = os.path.join(self.testrundir, "openldap-data")
self.ldap_uri = "ldap://%s:%d/" % (LOCALHOST, self._port)
if HAVE_LDAPI:
ldapi_path = os.path.join(self.testrundir, "ldapi")
self.ldapi_uri = "ldapi://%s" % quote_plus(ldapi_path)
self.default_ldap_uri = self.ldapi_uri
# use SASL/EXTERNAL via LDAPI when invoking OpenLDAP CLI tools
self.cli_sasl_external = ldap.SASL_AVAIL
else:
self.ldapi_uri = None
self.default_ldap_uri = self.ldap_uri
# Use simple bind via LDAP uri
self.cli_sasl_external = False
self._find_commands()
if self.SCHEMADIR is None:
raise ValueError("SCHEMADIR is None, ldap schemas are missing.")
# TLS certs
self.cafile = os.path.join(HERE, "certs/ca.pem")
self.servercert = os.path.join(HERE, "certs/server.pem")
self.serverkey = os.path.join(HERE, "certs/server.key")
self.clientcert = os.path.join(HERE, "certs/client.pem")
self.clientkey = os.path.join(HERE, "certs/client.key")
@property
def root_dn(self):
return "cn={self.root_cn},{self.suffix}".format(self=self)
def _find_commands(self):
self.PATH_LDAPADD = self._find_command("ldapadd")
self.PATH_LDAPDELETE = self._find_command("ldapdelete")
self.PATH_LDAPMODIFY = self._find_command("ldapmodify")
self.PATH_LDAPWHOAMI = self._find_command("ldapwhoami")
self.PATH_SLAPD = os.environ.get("SLAPD", None)
if not self.PATH_SLAPD:
self.PATH_SLAPD = self._find_command("slapd", in_sbin=True)
def _find_command(self, cmd, in_sbin=False):
if in_sbin:
path = self.SBIN_PATH
var_name = "SBIN"
else:
path = self.BIN_PATH
var_name = "BIN"
command = which(cmd, path=path)
if command is None:
raise ValueError(
"Command '{}' not found. Set the {} environment variable to "
"override slapdtest's search path.".format(cmd, var_name)
)
return command
def setup_rundir(self):
"""
creates rundir structure
for setting up a custom directory structure you have to override
this method
"""
os.mkdir(self.testrundir)
os.mkdir(self._db_directory)
self._create_sub_dirs(self.testrunsubdirs)
self._ln_schema_files(self.openldap_schema_files, self.SCHEMADIR)
def _cleanup_rundir(self):
"""
Recursively delete whole directory specified by `path'
"""
# cleanup_rundir() is called in atexit handler. Until Python 3.4,
# the rest of the world is already destroyed.
import os, os.path
if not os.path.exists(self.testrundir):
return
self._log.debug("clean-up %s", self.testrundir)
for dirpath, dirnames, filenames in os.walk(self.testrundir, topdown=False):
for filename in filenames:
self._log.debug("remove %s", os.path.join(dirpath, filename))
os.remove(os.path.join(dirpath, filename))
for dirname in dirnames:
self._log.debug("rmdir %s", os.path.join(dirpath, dirname))
os.rmdir(os.path.join(dirpath, dirname))
os.rmdir(self.testrundir)
self._log.info("cleaned-up %s", self.testrundir)
def _avail_tcp_port(self):
"""
find an available port for TCP connection
"""
sock = socket.socket()
try:
sock.bind((self.local_host, 0))
port = sock.getsockname()[1]
finally:
sock.close()
self._log.info("Found available port %d", port)
return port
def gen_config(self):
"""
generates a slapd.conf and returns it as one string
for generating specific static configuration files you have to
override this method
"""
include_directives = "\n".join(
'include "{schema_prefix}/{schema_file}"'.format(
schema_prefix=self._schema_prefix, schema_file=schema_file,
)
for schema_file in self.openldap_schema_files
)
config_dict = {
"serverid": hex(self.server_id),
"schema_prefix": self._schema_prefix,
"include_directives": include_directives,
"loglevel": self.slapd_loglevel,
"database": self.database,
"directory": self._db_directory,
"suffix": self.suffix,
"rootdn": self.root_dn,
"rootpw": self.root_pw,
"root_uid": os.getuid(),
"root_gid": os.getgid(),
"cafile": self.cafile,
"servercert": self.servercert,
"serverkey": self.serverkey,
}
return self.slapd_conf_template % config_dict
def _create_sub_dirs(self, dir_names):
"""
create sub-directories beneath self.testrundir
"""
for dname in dir_names:
dir_name = os.path.join(self.testrundir, dname)
self._log.debug("Create directory %s", dir_name)
os.mkdir(dir_name)
def _ln_schema_files(self, file_names, source_dir):
"""
write symbolic links to original schema files
"""
for fname in file_names:
ln_source = os.path.join(source_dir, fname)
ln_target = os.path.join(self._schema_prefix, fname)
self._log.debug("Create symlink %s -> %s", ln_source, ln_target)
os.symlink(ln_source, ln_target)
def _write_config(self):
"""Writes the slapd.conf file out, and returns the path to it."""
self._log.debug("Writing config to %s", self._slapd_conf)
with open(self._slapd_conf, "w") as config_file:
config_file.write(self.gen_config())
self._log.info("Wrote config to %s", self._slapd_conf)
def _test_config(self):
self._log.debug("testing config %s", self._slapd_conf)
popen_list = [
self.PATH_SLAPD,
"-Ttest",
"-f",
self._slapd_conf,
"-u",
]
if self._log.isEnabledFor(logging.DEBUG):
popen_list.append("-v")
popen_list.extend(["-d", "config"])
else:
popen_list.append("-Q")
proc = subprocess.Popen(popen_list)
if proc.wait() != 0:
raise RuntimeError("configuration test failed")
self._log.info("config ok: %s", self._slapd_conf)
def _start_slapd(self):
"""
Spawns/forks the slapd process
"""
urls = [self.ldap_uri]
if self.ldapi_uri:
urls.append(self.ldapi_uri)
slapd_args = [
self.PATH_SLAPD,
"-f",
self._slapd_conf,
"-F",
self.testrundir,
"-h",
" ".join(urls),
]
if self._log.isEnabledFor(logging.DEBUG):
slapd_args.extend(["-d", "-1"])
else:
slapd_args.extend(["-d", "0"])
self._log.info("starting slapd: %r", " ".join(slapd_args))
self._proc = subprocess.Popen(slapd_args)
# Waits until the LDAP server socket is open, or slapd crashed
# no cover to avoid spurious coverage changes, see
# https://github.com/python-ldap/python-ldap/issues/127
for _ in range(10): # pragma: no cover
if self._proc.poll() is not None:
self._stopped()
raise RuntimeError("slapd exited before opening port")
time.sleep(self._start_sleep)
try:
self._log.debug("slapd connection check to %s", self.default_ldap_uri)
self.ldapwhoami()
except RuntimeError:
pass
else:
return
raise RuntimeError("slapd did not start properly")
def start(self):
"""
Starts the slapd server process running, and waits for it to come up.
"""
if self._proc is None:
# prepare directory structure
atexit.register(self.stop)
self._cleanup_rundir()
self.setup_rundir()
self._write_config()
self._test_config()
self._start_slapd()
self._log.debug(
"slapd with pid=%d listening on %s and %s",
self._proc.pid,
self.ldap_uri,
self.ldapi_uri,
)
def stop(self):
"""
Stops the slapd server, and waits for it to terminate and cleans up
"""
if self._proc is not None:
self._log.debug("stopping slapd with pid %d", self._proc.pid)
self._proc.terminate()
self.wait()
self._cleanup_rundir()
if hasattr(atexit, "unregister"):
# Python 3
atexit.unregister(self.stop)
elif hasattr(atexit, "_exithandlers"):
# Python 2, can be None during process shutdown
try:
atexit._exithandlers.remove(self.stop)
except ValueError:
pass
def restart(self):
"""
Restarts the slapd server with same data
"""
self._proc.terminate()
self.wait()
self._start_slapd()
def wait(self):
"""Waits for the slapd process to terminate by itself."""
if self._proc:
self._proc.wait()
self._stopped()
def _stopped(self):
"""Called when the slapd server is known to have terminated"""
if self._proc is not None:
self._log.info("slapd[%d] terminated", self._proc.pid)
self._proc = None
def _cli_auth_args(self):
if self.cli_sasl_external:
authc_args = [
"-Y",
"EXTERNAL",
]
if not self._log.isEnabledFor(logging.DEBUG):
authc_args.append("-Q")
else:
authc_args = [
"-x",
"-D",
self.root_dn,
"-w",
self.root_pw,
]
return authc_args
# no cover to avoid spurious coverage changes
def _cli_popen(
self, ldapcommand, extra_args=None, ldap_uri=None, stdin_data=None
): # pragma: no cover
if ldap_uri is None:
ldap_uri = self.default_ldap_uri
args = (
[ldapcommand, "-H", ldap_uri,] + self._cli_auth_args() + (extra_args or [])
)
self._log.debug("Run command: %r", " ".join(args))
proc = subprocess.Popen(
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
self._log.debug("stdin_data=%r", stdin_data)
stdout_data, stderr_data = proc.communicate(stdin_data)
if stdout_data is not None:
self._log.debug("stdout_data=%r", stdout_data)
if stderr_data is not None:
self._log.debug("stderr_data=%r", stderr_data)
if proc.wait() != 0:
raise RuntimeError(
"{!r} process failed:\n{!r}\n{!r}".format(
args, stdout_data, stderr_data
)
)
return stdout_data, stderr_data
def ldapwhoami(self, extra_args=None):
"""
Runs ldapwhoami on this slapd instance
"""
self._cli_popen(self.PATH_LDAPWHOAMI, extra_args=extra_args)
def ldapadd(self, ldif, extra_args=None):
"""
Runs ldapadd on this slapd instance, passing it the ldif content
"""
self._cli_popen(
self.PATH_LDAPADD, extra_args=extra_args, stdin_data=ldif.encode("utf-8")
)
def ldapmodify(self, ldif, extra_args=None):
"""
Runs ldapadd on this slapd instance, passing it the ldif content
"""
self._cli_popen(
self.PATH_LDAPMODIFY, extra_args=extra_args, stdin_data=ldif.encode("utf-8")
)
def ldapdelete(self, dn, recursive=False, extra_args=None):
"""
Runs ldapdelete on this slapd instance, deleting 'dn'
"""
if extra_args is None:
extra_args = []
if recursive:
extra_args.append("-r")
extra_args.append(dn)
self._cli_popen(self.PATH_LDAPDELETE, extra_args=extra_args)
def __enter__(self):
self.start()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.stop()
class SlapdTestCase(unittest.TestCase):
"""
test class which also clones or initializes a running slapd
"""
server_class = SlapdObject
server = None
ldap_object_class = None
def _open_ldap_conn(self, who=None, cred=None, **kwargs):
"""
return a LDAPObject instance after simple bind
"""
ldap_conn = self.ldap_object_class(self.server.ldap_uri, **kwargs)
ldap_conn.protocol_version = 3
# ldap_conn.set_option(ldap.OPT_REFERRALS, 0)
ldap_conn.simple_bind_s(who or self.server.root_dn, cred or self.server.root_pw)
return ldap_conn
@classmethod
def setUpClass(cls):
cls.server = cls.server_class()
cls.server.start()
@classmethod
def tearDownClass(cls):
cls.server.stop()

View file

@ -0,0 +1,24 @@
python-ldap test certificates
=============================
Certificates and keys
---------------------
* ``ca.pem``: internal root CA certificate
* ``server.pem``: TLS server certificate for slapd, signed by root CA. The
server cert is valid for DNS Name ``localhost`` and IPs ``127.0.0.1`` and
``:1``.
* ``server.key``: private key for ``server.pem``, no password protection
* ``client.pem``: certificate for TLS client cert authentication, signed by
root CA.
* ``client.key``: private key for ``client.pem``, no password protection
Configuration and scripts
-------------------------
* ``ca.conf`` contains the CA definition as well as extensions for the
client and server certificates.
* ``client.conf`` and ``server.conf`` hold the subject and base configuration
for server and client certs.
* ``gencerts.sh`` creates new CA, client and server certificates.
* ``gennssdb.sh`` can be used to create a NSSDB for all certs and keys.

View file

@ -0,0 +1,77 @@
# Written by Christian Heimes
[default]
ca = "ca"
tmpdir = $ENV::CATMPDIR
outdir = $ENV::CAOUTDIR
name_opt = multiline,-esc_msb,utf8
[req]
default_bits = 2048
encrypt_key = no
default_md = sha256
utf8 = yes
string_mask = utf8only
prompt = no
distinguished_name = ca_dn
[ca_dn]
countryName = "DE"
organizationName = "python-ldap"
organizationalUnitName = "slapd-test"
commonName = "Python LDAP Test CA"
[ca]
default_ca = python_ldap_ca
[python_ldap_ca]
certificate = $outdir/$ca.pem
private_key = $outdir/$ca.key
new_certs_dir = $tmpdir
serial = $tmpdir/$ca.crt.srl
crlnumber = $tmpdir/$ca.crl.srl
database = $tmpdir/$ca.db
unique_subject = no
default_days = 365200
default_md = sha256
policy = match_pol
email_in_dn = no
preserve = no
name_opt = $name_opt
cert_opt = ca_default
copy_extensions = none
default_crl_days = 365100
[match_pol]
countryName = match
stateOrProvinceName = optional
localityName = optional
organizationName = match
organizationalUnitName = match
commonName = supplied
[ca_ext]
basicConstraints = critical,CA:true
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
[server_san]
DNS.1 = localhost
IP.1 = 127.0.0.1
IP.2 = ::1
[server_ext]
basicConstraints = critical,CA:false
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = critical,serverAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
subjectAltName = @server_san
[client_ext]
basicConstraints = critical,CA:false
keyUsage = critical,digitalSignature
extendedKeyUsage = critical,clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always

View file

@ -0,0 +1,80 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1 (0x1)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA
Validity
Not Before: Apr 12 18:52:38 2019 GMT
Not After : Oct 17 18:52:38 2994 GMT
Subject: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:d7:30:73:20:44:7d:83:d4:c7:01:b8:ab:1e:7c:
91:f4:38:ac:9c:41:43:64:0c:31:99:48:70:22:7d:
ae:1b:47:e7:2a:28:4d:f7:46:4e:b4:ba:ae:c0:9d:
d5:1f:4b:7a:79:2f:b9:dc:68:7f:79:84:88:50:51:
3b:7d:dc:d5:57:17:66:45:c0:2c:20:13:f7:99:d6:
9d:e2:12:7c:41:76:82:51:19:2c:b6:ff:46:cb:04:
56:38:22:2a:c3:7a:b5:71:51:49:4e:62:68:a0:99:
6f:de:f3:a2:0f:a2:aa:1b:72:a5:87:bc:42:5a:a7:
22:8d:33:b4:88:a8:dc:5d:72:ca:dd:a0:9a:4e:db:
7d:8b:10:de:c5:41:e9:e9:8d:fa:6c:dd:94:6e:b1:
31:c2:6d:a1:69:6c:7a:3a:b2:76:65:c9:e5:95:38:
62:40:81:c6:29:26:26:d1:d1:c1:f4:5e:fa:24:ef:
13:da:24:13:6f:f5:5c:ba:b1:31:8f:30:94:71:7b:
c6:e5:da:b9:b5:64:39:39:09:c2:4a:80:64:58:1d:
99:f5:65:3c:a7:26:08:95:26:35:7b:fa:e7:20:08:
ff:72:df:9b:8f:9f:da:8b:c3:a7:8b:fc:8c:c0:a5:
31:87:1d:4c:14:f6:cf:90:5e:2e:6e:a6:db:27:08:
eb:df
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
X509v3 Subject Key Identifier:
BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2
X509v3 Authority Key Identifier:
keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2
Signature Algorithm: sha256WithRSAEncryption
06:20:1f:eb:42:6a:42:62:b1:ee:69:c8:cd:47:a6:2e:69:95:
59:dc:49:09:69:40:93:25:a1:ec:6d:3a:dd:dc:e5:74:ab:33:
9d:8f:cc:e3:bb:7a:3f:5b:51:58:74:f7:bd:6c:7c:3c:b6:5a:
05:50:a8:8c:c3:fb:5b:75:2a:c2:6c:06:93:4c:a9:93:71:1c:
51:e5:be:a1:24:93:e2:79:ca:ea:08:86:90:b9:70:e7:7a:40:
bf:f4:d6:71:f4:4d:c0:0f:e0:31:a0:23:46:77:30:72:a9:62:
8a:2a:12:c4:dd:3d:86:ae:f7:6b:33:80:26:58:49:53:ff:cd:
8a:c6:f6:11:2c:b3:ff:a5:8e:1c:f8:22:e2:1b:8e:04:33:fb:
0d:da:31:86:12:9f:d1:03:86:9c:6a:78:5e:3c:5e:8a:52:aa:
68:1f:ff:f9:17:75:b0:da:f2:99:3c:80:3c:96:2a:33:07:54:
59:84:e7:92:34:0f:99:76:e3:d6:4d:4d:9c:fb:21:35:f9:cb:
a5:30:80:8b:9d:61:90:d3:d4:59:3a:2f:f2:f6:20:13:7e:26:
dc:50:b0:49:3e:19:fe:eb:7d:cf:b9:1a:5d:5c:3a:76:30:d9:
0e:d7:df:de:ce:a9:c4:21:df:63:b9:d0:64:86:0b:28:9a:2e:
ab:51:73:e4
-----BEGIN CERTIFICATE-----
MIIDjDCCAnSgAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU
MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV
BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMjk5NDEw
MTcxODUyMzhaMFYxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET
MBEGA1UECwwKc2xhcGQtdGVzdDEcMBoGA1UEAwwTUHl0aG9uIExEQVAgVGVzdCBD
QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANcwcyBEfYPUxwG4qx58
kfQ4rJxBQ2QMMZlIcCJ9rhtH5yooTfdGTrS6rsCd1R9Lenkvudxof3mEiFBRO33c
1VcXZkXALCAT95nWneISfEF2glEZLLb/RssEVjgiKsN6tXFRSU5iaKCZb97zog+i
qhtypYe8QlqnIo0ztIio3F1yyt2gmk7bfYsQ3sVB6emN+mzdlG6xMcJtoWlsejqy
dmXJ5ZU4YkCBxikmJtHRwfRe+iTvE9okE2/1XLqxMY8wlHF7xuXaubVkOTkJwkqA
ZFgdmfVlPKcmCJUmNXv65yAI/3Lfm4+f2ovDp4v8jMClMYcdTBT2z5BeLm6m2ycI
698CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
VR0OBBYEFL141UrxkJbF6OxmSSNHA18mc4ayMB8GA1UdIwQYMBaAFL141UrxkJbF
6OxmSSNHA18mc4ayMA0GCSqGSIb3DQEBCwUAA4IBAQAGIB/rQmpCYrHuacjNR6Yu
aZVZ3EkJaUCTJaHsbTrd3OV0qzOdj8zju3o/W1FYdPe9bHw8tloFUKiMw/tbdSrC
bAaTTKmTcRxR5b6hJJPiecrqCIaQuXDnekC/9NZx9E3AD+AxoCNGdzByqWKKKhLE
3T2GrvdrM4AmWElT/82KxvYRLLP/pY4c+CLiG44EM/sN2jGGEp/RA4acanhePF6K
UqpoH//5F3Ww2vKZPIA8liozB1RZhOeSNA+ZduPWTU2c+yE1+culMICLnWGQ09RZ
Oi/y9iATfibcULBJPhn+633PuRpdXDp2MNkO19/ezqnEId9judBkhgsomi6rUXPk
-----END CERTIFICATE-----

View file

@ -0,0 +1,16 @@
# Written by Christian Heimes
[req]
default_bits = 2048
encrypt_key = no
default_md = sha256
utf8 = yes
string_mask = utf8only
prompt = no
distinguished_name = client_dn
[client_dn]
countryName = "DE"
organizationName = "python-ldap"
organizationalUnitName = "slapd-test"
commonName = "client"

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDjt5O6nRrnAWPm
T0JvRLBHMclll92IWF/O4GEdcJ5fbBxP3BxK0Dv+6aRcR7b2o0f6fk/bgNepXfv/
MXDQcFlESbfmUNGshFmZr0sjPrYPD1R06TZs+/7RsMXnx1c79mFGEQ4wqzDOBHKQ
xeDhNJk+BcE0QABsqF8AA2XC2/dK14QCljKLC84k1zTFTnh8duN2eAalaPQFFOoj
4AnonUnswJ45zIx5V2BdG+oqO5dwo/cEukKgAEL8T2IJ9Cqlmh2sPbMqYC8cODq6
YcugMznxrfHV5LNThfkvwMe26+vv68r65zalPDy0M+cUMTMyBVY4TL3fejrloY2t
YMhPJIclAgMBAAECggEAPXdd/u9NRbGQX6hhTFuEIZOEw1F80MLaCaNzU1kExskN
01icom0W5LX4UZhiAK0OTsUtlRhwHh1qWfXkd777uX0UkKycDC8laGByra7Nwb7n
ky8oK77Rh5RptyiNmXflxd3wsJ5k7BczPXTMQL3L53vyLMJh2vKPwhcorrJlS+Pi
JjINMaR4IrDlpMYlrn9NTjsGr+mj/pdmKfU/KVXeKzFcwKTjUnDJNSbGDIC0AxaJ
dGU0yIX9MPW+p5szcA9o22UWW4LsEFY4YABeCqbm9/UQt3jWVMjCy4AOgr/9HWSR
DvXI/Xtdl3CTCr8+qDnhBaUI27z+UelZfTBFKUb8AQKBgQD6SmtrTBgEfb6tuxJw
AAHRuUcWGjatZ7X+meHRC9B7UPxUrKl9tU5NC7Gz6YMt+vr4bNMwykI6Ndj+4tSJ
KqsAC86v19CH4usMBLZ68MeTRvtQGiPah71syYrxf0uvYOx/KzUUBX240Ls+lEbE
W33psMoNAezUPpJwKx7CMjcBgQKBgQDo6VaT59bKRc3DXJvqFjd7TPIex+ny6JK+
8oOwyyFFBwkzfymoOxN4lxSrE6yf7uTemRRn+RIH3UGDottIDqzhjvtcV5uODeIN
8WzxTbl759qIxt+z7aF7SkwJLJAAZS3qqCXKtMBo7ln4xKaoRLT2RohqD1YXGrg8
wmYcUZoPpQKBgQCm2QVSuZ8pH0oFNjfMQbT0wbYJnd/lKMXBu4M1f9Ky4gHT0GYM
Ttirs6f6byfrduvmv2TpmWscsti80SktZywnE7fssMlqTHKzyFB9FBV2sFLHyyUr
gGFeK9xbsKgbeVkuTPdNKXvtv/eSd/XU38jIB/opQadGtY+ZBqWyfxb8AQKBgBLc
SlmBzZ/llSr7xdhn4ihG69hYQfacpL13r/hSCqinUDRuWLY5ynLacR8FYdY1pyzr
Yn6k6bPfU93QA0fLgG5ngK1SntMbBrIwWa0UqS+Cb+zhhd3xIUF1m8CmbibKCrTU
1vKaPnaAzqJZclFv9uN2hLdp9IO8cyzgZRpn9TzNAoGAUfZF1983qknfBgD8Lgm3
zzKYtc8q2Ukatfo4VCp66CEprbLcBq5mKx6JiBoMGqU8SI5XVG0F0aHH2n8gImcu
bO0vtEldDc1ylZ/H7xhHFWlMzmTlsbHdHVtetFfKLTpjq6duvgLA12lJNHNVu3OU
Z1bRWDeZIP70+jdYrmSoVi8=
-----END PRIVATE KEY-----

View file

@ -0,0 +1,83 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 3 (0x3)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA
Validity
Not Before: Apr 12 18:52:38 2019 GMT
Not After : Mar 1 18:52:38 3019 GMT
Subject: C=DE, O=python-ldap, OU=slapd-test, CN=client
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:e3:b7:93:ba:9d:1a:e7:01:63:e6:4f:42:6f:44:
b0:47:31:c9:65:97:dd:88:58:5f:ce:e0:61:1d:70:
9e:5f:6c:1c:4f:dc:1c:4a:d0:3b:fe:e9:a4:5c:47:
b6:f6:a3:47:fa:7e:4f:db:80:d7:a9:5d:fb:ff:31:
70:d0:70:59:44:49:b7:e6:50:d1:ac:84:59:99:af:
4b:23:3e:b6:0f:0f:54:74:e9:36:6c:fb:fe:d1:b0:
c5:e7:c7:57:3b:f6:61:46:11:0e:30:ab:30:ce:04:
72:90:c5:e0:e1:34:99:3e:05:c1:34:40:00:6c:a8:
5f:00:03:65:c2:db:f7:4a:d7:84:02:96:32:8b:0b:
ce:24:d7:34:c5:4e:78:7c:76:e3:76:78:06:a5:68:
f4:05:14:ea:23:e0:09:e8:9d:49:ec:c0:9e:39:cc:
8c:79:57:60:5d:1b:ea:2a:3b:97:70:a3:f7:04:ba:
42:a0:00:42:fc:4f:62:09:f4:2a:a5:9a:1d:ac:3d:
b3:2a:60:2f:1c:38:3a:ba:61:cb:a0:33:39:f1:ad:
f1:d5:e4:b3:53:85:f9:2f:c0:c7:b6:eb:eb:ef:eb:
ca:fa:e7:36:a5:3c:3c:b4:33:e7:14:31:33:32:05:
56:38:4c:bd:df:7a:3a:e5:a1:8d:ad:60:c8:4f:24:
87:25
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage: critical
TLS Web Client Authentication
X509v3 Subject Key Identifier:
4F:E7:35:C7:C8:C1:01:C3:7C:53:86:B9:BF:AE:8B:D6:45:A2:78:20
X509v3 Authority Key Identifier:
keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2
Signature Algorithm: sha256WithRSAEncryption
1c:90:5f:cf:18:48:95:4d:9d:d3:8e:6d:d1:69:19:1e:7b:3f:
1f:48:7c:c8:0d:2f:c4:53:0f:89:23:f4:be:ea:b4:7a:c6:dd:
cc:18:0f:e7:34:ea:2c:d4:07:0d:65:78:e8:20:40:3f:36:ef:
2c:00:31:69:e6:20:48:65:be:57:03:0e:69:ff:b9:83:59:99:
7d:4d:86:98:14:5b:8e:39:25:3a:a8:6d:51:dc:45:a5:0f:cd:
f3:7a:fd:55:af:5f:55:75:20:03:f5:4a:75:6a:79:2f:76:84:
f6:4e:3d:1d:59:45:9a:b1:6a:57:6f:16:76:76:f8:df:6e:96:
d5:25:27:34:4b:21:d8:c9:9a:36:55:45:a0:43:16:43:68:93:
37:af:81:89:06:d1:56:1b:9e:0f:62:40:ad:3c:4c:f5:ef:6c:
a2:a4:7f:f2:fa:78:9c:0d:c0:19:f1:10:e8:d8:cf:03:67:3c:
2d:4d:f3:5d:67:5c:41:a7:4f:d6:c5:0e:ff:2c:04:dd:23:bb:
85:44:8e:25:ac:15:a3:82:fa:a4:4f:fa:1d:87:f0:58:dc:ae:
53:05:b9:81:e8:cb:e5:0c:ac:a5:74:68:03:f9:22:a0:45:b6:
62:58:e0:98:d9:8c:54:a4:22:03:7a:37:12:eb:7d:b1:ad:45:
60:8e:7a:df
-----BEGIN CERTIFICATE-----
MIIDlDCCAnygAwIBAgIBAzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU
MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV
BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMzAxOTAz
MDExODUyMzhaMEkxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET
MBEGA1UECwwKc2xhcGQtdGVzdDEPMA0GA1UEAwwGY2xpZW50MIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA47eTup0a5wFj5k9Cb0SwRzHJZZfdiFhfzuBh
HXCeX2wcT9wcStA7/umkXEe29qNH+n5P24DXqV37/zFw0HBZREm35lDRrIRZma9L
Iz62Dw9UdOk2bPv+0bDF58dXO/ZhRhEOMKswzgRykMXg4TSZPgXBNEAAbKhfAANl
wtv3SteEApYyiwvOJNc0xU54fHbjdngGpWj0BRTqI+AJ6J1J7MCeOcyMeVdgXRvq
KjuXcKP3BLpCoABC/E9iCfQqpZodrD2zKmAvHDg6umHLoDM58a3x1eSzU4X5L8DH
tuvr7+vK+uc2pTw8tDPnFDEzMgVWOEy933o65aGNrWDITySHJQIDAQABo3gwdjAM
BgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEF
BQcDAjAdBgNVHQ4EFgQUT+c1x8jBAcN8U4a5v66L1kWieCAwHwYDVR0jBBgwFoAU
vXjVSvGQlsXo7GZJI0cDXyZzhrIwDQYJKoZIhvcNAQELBQADggEBAByQX88YSJVN
ndOObdFpGR57Px9IfMgNL8RTD4kj9L7qtHrG3cwYD+c06izUBw1leOggQD827ywA
MWnmIEhlvlcDDmn/uYNZmX1NhpgUW445JTqobVHcRaUPzfN6/VWvX1V1IAP1SnVq
eS92hPZOPR1ZRZqxaldvFnZ2+N9ultUlJzRLIdjJmjZVRaBDFkNokzevgYkG0VYb
ng9iQK08TPXvbKKkf/L6eJwNwBnxEOjYzwNnPC1N811nXEGnT9bFDv8sBN0ju4VE
jiWsFaOC+qRP+h2H8FjcrlMFuYHoy+UMrKV0aAP5IqBFtmJY4JjZjFSkIgN6NxLr
fbGtRWCOet8=
-----END CERTIFICATE-----

View file

@ -0,0 +1,68 @@
#!/bin/sh
# Written by Christian Heimes
set -e
export CAOUTDIR=.
export CATMPDIR=tmp
rm -rf $CATMPDIR
rm -rf ca.pem ca.key server.pem server.key client.pem client.key
rm -rf cert9.db key4.db pkcs11.tx
mkdir -p $CAOUTDIR
mkdir -p $CATMPDIR
touch $CATMPDIR/ca.db
touch $CATMPDIR/ca.db.attr
echo '01' > $CATMPDIR/ca.crt.srl
echo '01' > $CATMPDIR/ca.crl.srl
# root CA
openssl req -new \
-config ca.conf \
-out $CATMPDIR/ca.csr \
-keyout $CAOUTDIR/ca.key \
-batch
openssl ca -selfsign \
-config ca.conf \
-in $CATMPDIR/ca.csr \
-out $CAOUTDIR/ca.pem \
-extensions ca_ext \
-days 356300 \
-batch
# server cert
openssl req -new \
-config server.conf \
-out $CATMPDIR/server.csr \
-keyout $CAOUTDIR/server.key \
-batch
openssl ca \
-config ca.conf \
-in $CATMPDIR/server.csr \
-out $CAOUTDIR/server.pem \
-policy match_pol \
-extensions server_ext \
-batch
# client cert
openssl req -new \
-config client.conf \
-out $CATMPDIR/client.csr \
-keyout $CAOUTDIR/client.key \
-batch
openssl ca \
-config ca.conf \
-in $CATMPDIR/client.csr \
-out $CAOUTDIR/client.pem \
-policy match_pol \
-extensions client_ext \
-batch
# cleanup
rm -rf $CATMPDIR ca.key
echo DONE

View file

@ -0,0 +1,28 @@
#!/bin/sh
# Written by Christian Heimes
set -e
CATMPDIR=tmp
PASSFILE=${CATMPDIR}/passwd.txt
NSSDB=sql:${CAOUTDIR}
mkdir -p $CATMPDIR
# Create PKCS#12 files for NSSDB import
echo "dummy" > $PASSFILE
openssl pkcs12 -name "servercert" -in server.pem -inkey server.key \
-caname "testca" -CAfile ca.pem \
-password "file:${PASSFILE}" -export -out server.p12
openssl pkcs12 -name "clientcert" -in client.pem -inkey client.key \
-caname "testca" -CAfile ca.pem \
-password "file:${PASSFILE}" -export -out client.p12
# Create NSS DB
certutil -d $NSSDB -N --empty-password
certutil -d $NSSDB -A -n "testca" -t CT,, -a -i ca.pem
pk12util -d $NSSDB -i server.p12 -w ${PASSFILE}
pk12util -d $NSSDB -i client.p12 -w ${PASSFILE}
certutil -d $NSSDB -L
# cleanup
rm -rf $CATMPDIR server.p12 client.p12

View file

@ -0,0 +1,16 @@
# Written by Christian Heimes
[req]
default_bits = 2048
encrypt_key = no
default_md = sha256
utf8 = yes
string_mask = utf8only
prompt = no
distinguished_name = server_dn
[server_dn]
countryName = "DE"
organizationName = "python-ldap"
organizationalUnitName = "slapd-test"
commonName = "server cert for localhost"

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsBk0ml3ERFJyg
I6ujIJYERVU4doTZZd4r4z/LOef0hyiYiIQAc9wetaoZpM+bl4Eherxy9SBaCBwR
zefbaYQz2f2hdEDb+sISOiTke1eiF2ugYNlS55Wk1KnCnORE9bjcSNLPsscoUSzE
2bnBSoUwdiVK18YOCZR6GTeC8eA3ekvlR+9g+FBOgQ9+StXPDdq+iIAGXZREJIua
munErtTOw85De4YFCnzGw3UeCITDD4wFmI2IWphRFwWPsSDwUJfATA8S+7Rm4vwr
Qj726gUDlicTzPXKhJjXjj6XL7xXHfpQwMPkBCrxesKceHMJ+mrRsuuqHciuixRi
g94mILElAgMBAAECggEADG5oJOHMye8zYl8xiBhSvvxDrFDkSNGTvJgvhAArQwCB
boRvBZlZzt5R7Ih8eEH6kvDLrYMJU3hCjwbSOojlhNm7+m7sQPleDPMmt1wyeQQ4
Qt681cDmj4LOwcGUvWcEdObOVTQWMFOtaIxTYCSCe34OM9pj9Z+7mxc3a78O9PND
Ib/CwcTA1OyoupzkKirqkdLXwK3x2aT/1TMaPX94taHB51cxXc7AglL9QnuCkuaG
krqrexy3rGimzsP3OwQGEUjWKcZVSSPT8/k1pPE9hRgOqBy05BfkAzlebdvc3GO5
AbZk0NX2sfVHl4dTEXs/hTBCTQ3XmaltumQ9MdL+AQKBgQDg2I5QxBA2UHb8vCtK
f31kfG6YQc4MkoslrrMrtJjZqDYaLZPS1ARPSfYRqcc+7GDreuLmw39f8ZECd+2W
BYUqzZv9g13R9DY99g0/sINnZGsESwfIdLNNlHvVx2UrD5ybCj4vLhuPsVV7XlWs
cpl+rcuBVpqy8UIXifQ/Z3xLvwKBgQDD3CLjuC0mcTO2sIWqEHqVkc8CY2NJA2Qh
C78fwpaCqJUUdWnS69QbRGWgkFJL+oO8lQVQ1bXhZLHyQmy7Z5d5olCH6AW4GRnf
hBAnKJ+QTm9B6QVWzjUuHuOeCukfiTQbha14pOS9ar3X2QFWjDnzCRrnAxJmoY3H
BJATLHhMGwKBgQDSxAy7xt4Pm+O9y8Gk5tcq771X+i9k96V54EZRzMuPFDAK3/h2
o4marZD9Q7Hi2P+NHTc+67klvbKZpsPOYkRPOEdmH9M9cPe7oz8OGa9DpwzuDEsy
a7p8GZjvbyb1c3/wkWxzG3x4eNnReD9FFHOwHMfr6LvAy4iRuh57pM0NzwKBgDY3
1DixnV4M7EHgb7/6O9T3vhRtKujlVWyIcen61etpe4tkTV0kB11c+70M9pstyBYG
MqiD4It6coAbvznJnXcAZcaZhivGVxE237nXVwR9kfLu7JlxD+uqhVwUrSAbvR75
TGIfU2rUB6We3u30d349wQK+KPPcOQEk1DValBqNAoGBAKfXOXgFBkIVW79fOkup
aIZXdEmU3Up61Oo0KDbxsg4l73NnnvuEnNMBTx3nT3KCVIAcQL9MNpLX/Z0HjOn1
aiWVtTNq2OFL0V0HueBhbkFiWp551jTS7LjndCYHpUB/B8/wXP0kxHUm8HrQrRvK
DhV3zcxsXts1INidXjzzOkPi
-----END PRIVATE KEY-----

View file

@ -0,0 +1,86 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 2 (0x2)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA
Validity
Not Before: Apr 12 18:52:38 2019 GMT
Not After : Mar 1 18:52:38 3019 GMT
Subject: C=DE, O=python-ldap, OU=slapd-test, CN=server cert for localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:ac:06:4d:26:97:71:11:14:9c:a0:23:ab:a3:20:
96:04:45:55:38:76:84:d9:65:de:2b:e3:3f:cb:39:
e7:f4:87:28:98:88:84:00:73:dc:1e:b5:aa:19:a4:
cf:9b:97:81:21:7a:bc:72:f5:20:5a:08:1c:11:cd:
e7:db:69:84:33:d9:fd:a1:74:40:db:fa:c2:12:3a:
24:e4:7b:57:a2:17:6b:a0:60:d9:52:e7:95:a4:d4:
a9:c2:9c:e4:44:f5:b8:dc:48:d2:cf:b2:c7:28:51:
2c:c4:d9:b9:c1:4a:85:30:76:25:4a:d7:c6:0e:09:
94:7a:19:37:82:f1:e0:37:7a:4b:e5:47:ef:60:f8:
50:4e:81:0f:7e:4a:d5:cf:0d:da:be:88:80:06:5d:
94:44:24:8b:9a:9a:e9:c4:ae:d4:ce:c3:ce:43:7b:
86:05:0a:7c:c6:c3:75:1e:08:84:c3:0f:8c:05:98:
8d:88:5a:98:51:17:05:8f:b1:20:f0:50:97:c0:4c:
0f:12:fb:b4:66:e2:fc:2b:42:3e:f6:ea:05:03:96:
27:13:cc:f5:ca:84:98:d7:8e:3e:97:2f:bc:57:1d:
fa:50:c0:c3:e4:04:2a:f1:7a:c2:9c:78:73:09:fa:
6a:d1:b2:eb:aa:1d:c8:ae:8b:14:62:83:de:26:20:
b1:25
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage: critical
TLS Web Server Authentication
X509v3 Subject Key Identifier:
08:D1:86:1B:82:0A:4F:71:31:E4:F5:31:23:CC:67:3B:FA:84:3B:A0
X509v3 Authority Key Identifier:
keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2
X509v3 Subject Alternative Name:
DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
Signature Algorithm: sha256WithRSAEncryption
88:60:af:be:11:c4:aa:dc:9b:f1:e7:14:da:20:aa:6f:2f:06:
ae:38:b2:7c:ac:90:81:22:51:7e:cb:26:15:6e:fe:67:98:c1:
0d:dc:aa:39:98:2b:d2:cc:3c:ff:1a:92:2f:56:0a:a9:6e:d8:
9a:3d:c5:4d:6f:cc:91:2e:e3:4e:bf:22:ab:cb:92:1a:a0:8f:
43:cd:82:bc:48:55:c4:95:cf:10:6b:6a:31:19:92:7d:e0:06:
05:6f:0b:33:e7:2a:37:42:f9:ec:1b:29:99:e1:58:0c:01:a7:
c3:8b:58:71:21:9f:61:8c:a7:fb:b6:7e:32:8b:a9:4e:c7:1f:
f6:46:e8:dd:ac:a6:4c:53:f8:4d:93:e4:ec:73:ab:0b:be:98:
c5:78:c4:92:c0:4c:78:47:52:2f:93:07:67:20:a4:5a:7f:59:
7e:4f:48:53:20:0d:37:bb:06:f8:44:42:64:b4:94:15:43:d1:
4c:51:f3:97:1d:2d:cd:db:b9:bb:1a:69:10:89:7d:ae:1d:0d:
94:78:45:29:cd:c4:42:67:67:96:05:bf:da:aa:23:65:7b:04:
ff:b7:ac:9d:ee:0b:e7:0f:c1:c5:0b:48:fe:0f:d6:3f:d8:b4:
77:12:bb:f5:91:4f:43:e6:01:3f:a4:c0:ea:8c:c6:68:99:8e:
49:e8:c4:8b
-----BEGIN CERTIFICATE-----
MIID1zCCAr+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU
MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV
BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMzAxOTAz
MDExODUyMzhaMFwxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET
MBEGA1UECwwKc2xhcGQtdGVzdDEiMCAGA1UEAwwZc2VydmVyIGNlcnQgZm9yIGxv
Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKwGTSaXcREU
nKAjq6MglgRFVTh2hNll3ivjP8s55/SHKJiIhABz3B61qhmkz5uXgSF6vHL1IFoI
HBHN59tphDPZ/aF0QNv6whI6JOR7V6IXa6Bg2VLnlaTUqcKc5ET1uNxI0s+yxyhR
LMTZucFKhTB2JUrXxg4JlHoZN4Lx4Dd6S+VH72D4UE6BD35K1c8N2r6IgAZdlEQk
i5qa6cSu1M7DzkN7hgUKfMbDdR4IhMMPjAWYjYhamFEXBY+xIPBQl8BMDxL7tGbi
/CtCPvbqBQOWJxPM9cqEmNeOPpcvvFcd+lDAw+QEKvF6wpx4cwn6atGy66odyK6L
FGKD3iYgsSUCAwEAAaOBpzCBpDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF
oDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUCNGGG4IKT3Ex5PUx
I8xnO/qEO6AwHwYDVR0jBBgwFoAUvXjVSvGQlsXo7GZJI0cDXyZzhrIwLAYDVR0R
BCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3
DQEBCwUAA4IBAQCIYK++EcSq3Jvx5xTaIKpvLwauOLJ8rJCBIlF+yyYVbv5nmMEN
3Ko5mCvSzDz/GpIvVgqpbtiaPcVNb8yRLuNOvyKry5IaoI9DzYK8SFXElc8Qa2ox
GZJ94AYFbwsz5yo3QvnsGymZ4VgMAafDi1hxIZ9hjKf7tn4yi6lOxx/2RujdrKZM
U/hNk+Tsc6sLvpjFeMSSwEx4R1IvkwdnIKRaf1l+T0hTIA03uwb4REJktJQVQ9FM
UfOXHS3N27m7GmkQiX2uHQ2UeEUpzcRCZ2eWBb/aqiNlewT/t6yd7gvnD8HFC0j+
D9Y/2LR3Erv1kU9D5gE/pMDqjMZomY5J6MSL
-----END CERTIFICATE-----

View file

@ -2,9 +2,53 @@ def testauth_none():
return "some_data_from_none"
def testauth_subcat_none():
return "some_data_from_subcat_none"
def testauth_default():
return "some_data_from_default"
def testauth_subcat_default():
return "some_data_from_subcat_default"
def testauth_subcat_post():
return "some_data_from_subcat_post"
def testauth_other_profile():
return "some_data_from_other_profile"
def testauth_subcat_other_profile():
return "some_data_from_subcat_other_profile"
def testauth_only_api():
return "some_data_from_only_api"
def testauth_only_cli():
return "some_data_from_only_cli"
def testauth_ldap():
return "some_data_from_ldap"
def testauth_with_arg(super_arg):
return super_arg
def testauth_with_extra_str_only(only_a_str):
return only_a_str
def testauth_with_type_int(only_an_int):
return only_an_int
def yoloswag_version(*args, **kwargs):
return "666"

View file

@ -3,12 +3,17 @@ import pytest
from moulinette.actionsmap import (
CommentParameter,
AskParameter,
PasswordParameter,
PatternParameter,
RequiredParameter,
ExtraArgumentParser,
ActionsMap,
)
from moulinette.interfaces import GLOBAL_SECTION
from moulinette.interfaces import BaseActionsMapParser
from moulinette.core import MoulinetteError
from moulinette import m18n
@pytest.fixture
@ -34,6 +39,12 @@ def test_comment_parameter_bad_type(iface):
comment.validate({}, "b")
def test_ask_parameter_str_value(iface, caplog):
ask = AskParameter(iface)
assert ask.validate("a", "a") == "a"
assert not len(caplog.messages)
def test_ask_parameter_bad_bool_value(iface, caplog):
ask = AskParameter(iface)
assert ask.validate(True, "a") == "a"
@ -52,6 +63,32 @@ def test_ask_parameter_bad_type(iface):
ask.validate({}, "b")
def test_ask_parameter(iface, mocker):
ask = AskParameter(iface)
arg = ask("foobar", "a", "a")
assert arg == "a"
from moulinette.core import Moulinette18n, MoulinetteSignals
mocker.patch.object(Moulinette18n, "n", return_value="awesome_test")
mocker.patch.object(MoulinetteSignals, "prompt", return_value="awesome_test")
arg = ask("foobar", "a", None)
assert arg == "awesome_test"
def test_password_parameter(iface, mocker):
ask = PasswordParameter(iface)
arg = ask("foobar", "a", "a")
assert arg == "a"
from moulinette.core import Moulinette18n, MoulinetteSignals
mocker.patch.object(Moulinette18n, "n", return_value="awesome_test")
mocker.patch.object(MoulinetteSignals, "prompt", return_value="awesome_test")
arg = ask("foobar", "a", None)
assert arg == "awesome_test"
def test_pattern_parameter_bad_str_value(iface, caplog):
pattern = PatternParameter(iface)
assert pattern.validate("", "a") == ["", "pattern_not_match"]
@ -67,11 +104,56 @@ def test_pattern_parameter_bad_list_len(iface):
pattern.validate(iface, "a")
def test_required_paremeter_missing_value(iface):
def test_pattern_parameter(iface, caplog, mocker):
pattern = PatternParameter(iface)
arg = pattern(["foo", "foobar"], "foo_name", "foo_value")
assert arg == "foo_value"
error = "error_message"
mocker.patch("moulinette.Moulinette18n.n", return_value=error)
with pytest.raises(MoulinetteError) as exception:
pattern(["foo", "message"], "foo_name", "not_match")
translation = m18n.g("invalid_argument", argument="foo_name", error=error)
expected_msg = translation.format(argument="foo_name", error=error)
assert expected_msg in str(exception)
assert any("doesn't match pattern" in message for message in caplog.messages)
def test_required_paremeter(iface):
required = RequiredParameter(iface)
arg = required(True, "a", "a")
assert arg == "a"
assert required.validate(True, "a")
assert not required.validate(False, "a")
def test_required_paremeter_bad_type(iface):
required = RequiredParameter(iface)
with pytest.raises(TypeError):
required.validate("a", "a")
with pytest.raises(TypeError):
required.validate(1, "a")
with pytest.raises(TypeError):
required.validate([], "a")
with pytest.raises(TypeError):
required.validate({}, "a")
def test_required_paremeter_missing_value(iface, caplog):
required = RequiredParameter(iface)
with pytest.raises(MoulinetteError) as exception:
required(True, "a", "")
assert "is required" in str(exception)
translation = m18n.g("argument_required", argument="a")
expected_msg = translation.format(argument="a")
assert expected_msg in str(exception)
assert any("is required" in message for message in caplog.messages)
def test_actions_map_unknown_authenticator(monkeypatch, tmp_path):
@ -83,3 +165,151 @@ def test_actions_map_unknown_authenticator(monkeypatch, tmp_path):
with pytest.raises(ValueError) as exception:
amap.get_authenticator_for_profile("unknown")
assert "Unknown authenticator" in str(exception)
def test_extra_argument_parser_add_argument(iface):
extra_argument_parse = ExtraArgumentParser(iface)
extra_argument_parse.add_argument("Test", "foo", {"ask": "lol"})
assert "Test" in extra_argument_parse._extra_params
assert "foo" in extra_argument_parse._extra_params["Test"]
assert "ask" in extra_argument_parse._extra_params["Test"]["foo"]
assert extra_argument_parse._extra_params["Test"]["foo"]["ask"] == "lol"
extra_argument_parse = ExtraArgumentParser(iface)
extra_argument_parse.add_argument(GLOBAL_SECTION, "foo", {"ask": "lol"})
assert GLOBAL_SECTION in extra_argument_parse._extra_params
assert "foo" in extra_argument_parse._extra_params[GLOBAL_SECTION]
assert "ask" in extra_argument_parse._extra_params[GLOBAL_SECTION]["foo"]
assert extra_argument_parse._extra_params[GLOBAL_SECTION]["foo"]["ask"] == "lol"
def test_extra_argument_parser_add_argument_bad_arg(iface):
extra_argument_parse = ExtraArgumentParser(iface)
with pytest.raises(MoulinetteError) as exception:
extra_argument_parse.add_argument(GLOBAL_SECTION, "foo", {"ask": 1})
translation = m18n.g("error_see_log")
expected_msg = translation.format()
assert expected_msg in str(exception)
extra_argument_parse = ExtraArgumentParser(iface)
extra_argument_parse.add_argument(GLOBAL_SECTION, "foo", {"error": 1})
assert GLOBAL_SECTION in extra_argument_parse._extra_params
assert "foo" in extra_argument_parse._extra_params[GLOBAL_SECTION]
assert not len(extra_argument_parse._extra_params[GLOBAL_SECTION]["foo"])
def test_extra_argument_parser_parse_args(iface, mocker):
extra_argument_parse = ExtraArgumentParser(iface)
extra_argument_parse.add_argument(GLOBAL_SECTION, "foo", {"ask": "lol"})
extra_argument_parse.add_argument(GLOBAL_SECTION, "foo2", {"ask": "lol2"})
extra_argument_parse.add_argument(
GLOBAL_SECTION, "bar", {"password": "lul", "ask": "lul"}
)
args = extra_argument_parse.parse_args(
GLOBAL_SECTION, {"foo": 1, "foo2": ["a", "b", {"foobar": True}], "bar": "rab"}
)
assert "foo" in args
assert args["foo"] == 1
assert "foo2" in args
assert args["foo2"] == ["a", "b", {"foobar": True}]
assert "bar" in args
assert args["bar"] == "rab"
def test_actions_map_api():
from moulinette.interfaces.api import ActionsMapParser
amap = ActionsMap(ActionsMapParser, use_cache=False)
assert amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"]
assert "yoloswag" in amap.parser.global_conf["authenticator"]
assert ("GET", "/test-auth/default") in amap.parser.routes
assert ("POST", "/test-auth/subcat/post") in amap.parser.routes
amap.generate_cache()
amap = ActionsMap(ActionsMapParser, use_cache=True)
assert amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"]
assert "yoloswag" in amap.parser.global_conf["authenticator"]
assert ("GET", "/test-auth/default") in amap.parser.routes
assert ("POST", "/test-auth/subcat/post") in amap.parser.routes
def test_actions_map_import_error(mocker):
from moulinette.interfaces.api import ActionsMapParser
amap = ActionsMap(ActionsMapParser)
from moulinette.core import MoulinetteLock
mocker.patch.object(MoulinetteLock, "_is_son_of", return_value=False)
mocker.patch("__builtin__.__import__", side_effect=ImportError)
with pytest.raises(MoulinetteError) as exception:
amap.process({}, timeout=30, route=("GET", "/test-auth/none"))
mocker.stopall()
translation = m18n.g("error_see_log")
expected_msg = translation.format()
assert expected_msg in str(exception)
def test_actions_map_cli():
from moulinette.interfaces.cli import ActionsMapParser
import argparse
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
"--debug",
action="store_true",
default=False,
help="Log and print debug messages",
)
amap = ActionsMap(
ActionsMapParser, use_cache=False, parser_kwargs={"top_parser": parser}
)
assert amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"]
assert "yoloswag" in amap.parser.global_conf["authenticator"]
assert "testauth" in amap.parser._subparsers.choices
assert "none" in amap.parser._subparsers.choices["testauth"]._actions[1].choices
assert "subcat" in amap.parser._subparsers.choices["testauth"]._actions[1].choices
assert (
"default"
in amap.parser._subparsers.choices["testauth"]
._actions[1]
.choices["subcat"]
._actions[1]
.choices
)
amap.generate_cache()
amap = ActionsMap(
ActionsMapParser, use_cache=True, parser_kwargs={"top_parser": parser}
)
assert amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"]
assert "yoloswag" in amap.parser.global_conf["authenticator"]
assert "testauth" in amap.parser._subparsers.choices
assert "none" in amap.parser._subparsers.choices["testauth"]._actions[1].choices
assert "subcat" in amap.parser._subparsers.choices["testauth"]._actions[1].choices
assert (
"default"
in amap.parser._subparsers.choices["testauth"]
._actions[1]
.choices["subcat"]
._actions[1]
.choices
)

View file

@ -1,9 +1,13 @@
import os
import pytest
from moulinette import MoulinetteError
from moulinette import m18n
def login(webapi, csrf=False, profile=None, status=200):
data = {"password": "Yoloswag"}
class TestAuthAPI:
def login(self, webapi, csrf=False, profile=None, status=200, password="default"):
data = {"password": password}
if profile:
data["profile"] = profile
@ -14,26 +18,51 @@ def login(webapi, csrf=False, profile=None, status=200):
headers=None if csrf else {"X-Requested-With": ""},
)
def test_request_no_auth_needed(moulinette_webapi):
def test_request_no_auth_needed(self, moulinette_webapi):
assert (
moulinette_webapi.get("/test-auth/none", status=200).text
== '"some_data_from_none"'
)
def test_request_no_auth_needed_subcategories(self, moulinette_webapi):
assert (
moulinette_webapi.get("/test-auth/subcat/none", status=200).text
== '"some_data_from_subcat_none"'
)
def test_request_with_auth_but_not_logged(moulinette_webapi):
def test_request_with_auth_but_not_logged(self, moulinette_webapi):
assert (
moulinette_webapi.get("/test-auth/default", status=401).text
== "Authentication required"
)
def test_request_with_auth_subcategories_but_not_logged(self, moulinette_webapi):
assert (
moulinette_webapi.get("/test-auth/subcat/default", status=401).text
== "Authentication required"
)
def test_login(moulinette_webapi):
def test_request_not_logged_only_api(self, moulinette_webapi):
assert (
moulinette_webapi.get("/test-auth/only-api", status=401).text
== "Authentication required"
)
assert login(moulinette_webapi).text == "Logged in"
def test_request_only_api(self, moulinette_webapi):
self.login(moulinette_webapi)
assert (
moulinette_webapi.get("/test-auth/only-api", status=200).text
== '"some_data_from_only_api"'
)
def test_request_not_logged_only_cli(self, moulinette_webapi):
assert (
moulinette_webapi.get("/test-auth/only-cli", status=200).text
== '"some_data_from_only_cli"'
)
def test_login(self, moulinette_webapi):
assert self.login(moulinette_webapi).text == "Logged in"
assert "session.id" in moulinette_webapi.cookies
assert "session.tokens" in moulinette_webapi.cookies
@ -43,40 +72,49 @@ def test_login(moulinette_webapi):
cache_session_default
)
def test_login_bad_password(self, moulinette_webapi):
assert (
self.login(moulinette_webapi, password="Bad Password", status=401).text
== "Invalid password"
)
def test_login_csrf_attempt(moulinette_webapi):
assert "session.id" not in moulinette_webapi.cookies
assert "session.tokens" not in moulinette_webapi.cookies
def test_login_csrf_attempt(self, moulinette_webapi):
# C.f.
# https://security.stackexchange.com/a/58308
# https://stackoverflow.com/a/22533680
assert "CSRF protection" in login(moulinette_webapi, csrf=True, status=403).text
assert (
"CSRF protection"
in self.login(moulinette_webapi, csrf=True, status=403).text
)
assert not any(c.name == "session.id" for c in moulinette_webapi.cookiejar)
assert not any(c.name == "session.tokens" for c in moulinette_webapi.cookiejar)
def test_login_then_legit_request_without_cookies(moulinette_webapi):
login(moulinette_webapi)
def test_login_then_legit_request_without_cookies(self, moulinette_webapi):
self.login(moulinette_webapi)
moulinette_webapi.cookiejar.clear()
moulinette_webapi.get("/test-auth/default", status=401)
def test_login_then_legit_request(moulinette_webapi):
login(moulinette_webapi)
def test_login_then_legit_request(self, moulinette_webapi):
self.login(moulinette_webapi)
assert (
moulinette_webapi.get("/test-auth/default", status=200).text
== '"some_data_from_default"'
)
assert (
moulinette_webapi.get("/test-auth/subcat/default", status=200).text
== '"some_data_from_subcat_default"'
)
def test_login_then_logout(moulinette_webapi):
login(moulinette_webapi)
def test_login_then_logout(self, moulinette_webapi):
self.login(moulinette_webapi)
moulinette_webapi.get("/logout", status=200)
@ -89,3 +127,217 @@ def test_login_then_logout(moulinette_webapi):
moulinette_webapi.get("/test-auth/default", status=401).text
== "Authentication required"
)
def test_login_other_profile(self, moulinette_webapi):
self.login(moulinette_webapi, profile="yoloswag", password="yoloswag")
assert "session.id" in moulinette_webapi.cookies
assert "session.tokens" in moulinette_webapi.cookies
cache_session_default = (
os.environ["MOULINETTE_CACHE_DIR"] + "/session/yoloswag/"
)
assert moulinette_webapi.cookies["session.id"] + ".asc" in os.listdir(
cache_session_default
)
def test_login_wrong_profile(self, moulinette_webapi):
self.login(moulinette_webapi)
assert (
moulinette_webapi.get("/test-auth/other-profile", status=401).text
== "Authentication required"
)
moulinette_webapi.get("/logout", status=200)
self.login(moulinette_webapi, profile="yoloswag", password="yoloswag")
assert (
moulinette_webapi.get("/test-auth/default", status=401).text
== "Authentication required"
)
def test_login_ldap(self, moulinette_webapi, ldap_server, mocker):
mocker.patch(
"moulinette.authenticators.ldap.Authenticator._get_uri",
return_value=ldap_server.uri,
)
self.login(moulinette_webapi, profile="ldap", password="yunohost")
assert (
moulinette_webapi.get("/test-auth/ldap", status=200).text
== '"some_data_from_ldap"'
)
def test_request_with_arg(self, moulinette_webapi, capsys):
self.login(moulinette_webapi)
assert (
moulinette_webapi.get("/test-auth/with_arg/yoloswag", status=200).text
== '"yoloswag"'
)
def test_request_arg_with_extra(self, moulinette_webapi, caplog, mocker):
self.login(moulinette_webapi)
assert (
moulinette_webapi.get(
"/test-auth/with_extra_str_only/YoLoSwAg", status=200
).text
== '"YoLoSwAg"'
)
error = "error_message"
mocker.patch("moulinette.Moulinette18n.n", return_value=error)
moulinette_webapi.get("/test-auth/with_extra_str_only/12345", status=400)
assert any("doesn't match pattern" in message for message in caplog.messages)
def test_request_arg_with_type(self, moulinette_webapi, caplog, mocker):
self.login(moulinette_webapi)
assert (
moulinette_webapi.get("/test-auth/with_type_int/12345", status=200).text
== "12345"
)
error = "error_message"
mocker.patch("moulinette.Moulinette18n.g", return_value=error)
moulinette_webapi.get("/test-auth/with_type_int/yoloswag", status=400)
class TestAuthCLI:
def test_login(self, moulinette_cli, capsys, mocker):
mocker.patch("getpass.getpass", return_value="default")
moulinette_cli.run(["testauth", "default"], output_as="plain")
message = capsys.readouterr()
assert "some_data_from_default" in message.out
moulinette_cli.run(
["testauth", "default"], output_as="plain", password="default"
)
message = capsys.readouterr()
assert "some_data_from_default" in message.out
def test_login_bad_password(self, moulinette_cli, capsys, mocker):
with pytest.raises(MoulinetteError):
moulinette_cli.run(
["testauth", "default"], output_as="plain", password="Bad Password"
)
mocker.patch("getpass.getpass", return_value="Bad Password")
with pytest.raises(MoulinetteError):
moulinette_cli.run(["testauth", "default"], output_as="plain")
def test_login_wrong_profile(self, moulinette_cli, mocker):
mocker.patch("getpass.getpass", return_value="default")
with pytest.raises(MoulinetteError) as exception:
moulinette_cli.run(["testauth", "other-profile"], output_as="none")
translation = m18n.g("invalid_password")
expected_msg = translation.format()
assert expected_msg in str(exception)
with pytest.raises(MoulinetteError) as exception:
moulinette_cli.run(
["testauth", "default"], output_as="none", password="yoloswag"
)
expected_msg = translation.format()
assert expected_msg in str(exception)
def test_request_no_auth_needed(self, capsys, moulinette_cli):
moulinette_cli.run(["testauth", "none"], output_as="plain")
message = capsys.readouterr()
assert "some_data_from_none" in message.out
def test_request_not_logged_only_api(self, capsys, moulinette_cli):
moulinette_cli.run(["testauth", "only-api"], output_as="plain")
message = capsys.readouterr()
assert "some_data_from_only_api" in message.out
def test_request_only_cli(self, capsys, moulinette_cli, mocker):
mocker.patch("getpass.getpass", return_value="default")
moulinette_cli.run(["testauth", "only-cli"], output_as="plain")
message = capsys.readouterr()
assert "some_data_from_only_cli" in message.out
def test_request_not_logged_only_cli(self, capsys, moulinette_cli, mocker):
mocker.patch("getpass.getpass")
with pytest.raises(MoulinetteError) as exception:
moulinette_cli.run(["testauth", "only-cli"], output_as="plain")
message = capsys.readouterr()
assert "some_data_from_only_cli" not in message.out
translation = m18n.g("invalid_password")
expected_msg = translation.format()
assert expected_msg in str(exception)
def test_request_with_callback(self, moulinette_cli, capsys, mocker):
mocker.patch("getpass.getpass", return_value="default")
moulinette_cli.run(["--version"], output_as="plain")
message = capsys.readouterr()
assert "666" in message.out
moulinette_cli.run(["-v"], output_as="plain")
message = capsys.readouterr()
assert "666" in message.out
with pytest.raises(MoulinetteError):
moulinette_cli.run(["--wersion"], output_as="plain")
message = capsys.readouterr()
assert "cannot get value from callback method" in message.err
def test_request_with_arg(self, moulinette_cli, capsys, mocker):
mocker.patch("getpass.getpass", return_value="default")
moulinette_cli.run(["testauth", "with_arg", "yoloswag"], output_as="plain")
message = capsys.readouterr()
assert "yoloswag" in message.out
def test_request_arg_with_extra(self, moulinette_cli, capsys, mocker):
mocker.patch("getpass.getpass", return_value="default")
moulinette_cli.run(
["testauth", "with_extra_str_only", "YoLoSwAg"], output_as="plain"
)
message = capsys.readouterr()
assert "YoLoSwAg" in message.out
error = "error_message"
mocker.patch("moulinette.Moulinette18n.n", return_value=error)
with pytest.raises(MoulinetteError):
moulinette_cli.run(
["testauth", "with_extra_str_only", "12345"], output_as="plain"
)
message = capsys.readouterr()
assert "doesn't match pattern" in message.err
def test_request_arg_with_type(self, moulinette_cli, capsys, mocker):
mocker.patch("getpass.getpass", return_value="default")
moulinette_cli.run(["testauth", "with_type_int", "12345"], output_as="plain")
message = capsys.readouterr()
assert "12345" in message.out
mocker.patch("sys.exit")
with pytest.raises(MoulinetteError):
moulinette_cli.run(
["testauth", "with_type_int", "yoloswag"], output_as="plain"
)
message = capsys.readouterr()
assert "invalid int value" in message.err

View file

@ -1,6 +1,8 @@
import os
import pytest
import pwd
import grp
from moulinette import m18n
from moulinette.core import MoulinetteError
@ -8,9 +10,16 @@ from moulinette.utils.filesystem import (
append_to_file,
read_file,
read_json,
read_yaml,
read_toml,
read_ldif,
rm,
write_to_file,
write_to_json,
write_to_yaml,
mkdir,
chown,
chmod,
)
@ -42,6 +51,18 @@ def test_read_file_cannot_read_ioerror(test_file, mocker):
assert expected_msg in str(exception)
def test_read_file_cannot_read_exception(test_file, mocker):
error = "foobar"
mocker.patch("__builtin__.open", side_effect=Exception(error))
with pytest.raises(MoulinetteError) as exception:
read_file(str(test_file))
translation = m18n.g("unknown_error_reading_file", file=str(test_file), error=error)
expected_msg = translation.format(file=str(test_file), error=error)
assert expected_msg in str(exception)
def test_read_json(test_json):
content = read_json(str(test_json))
assert "foo" in content.keys()
@ -60,6 +81,82 @@ def test_read_json_cannot_read(test_json, mocker):
assert expected_msg in str(exception)
def test_read_yaml(test_yaml):
content = read_yaml(str(test_yaml))
assert "foo" in content.keys()
assert content["foo"] == "bar"
def test_read_yaml_cannot_read(test_yaml, mocker):
error = "foobar"
mocker.patch("yaml.safe_load", side_effect=Exception(error))
with pytest.raises(MoulinetteError) as exception:
read_yaml(str(test_yaml))
translation = m18n.g("corrupted_yaml", ressource=str(test_yaml), error=error)
expected_msg = translation.format(ressource=str(test_yaml), error=error)
assert expected_msg in str(exception)
def test_read_toml(test_toml):
content = read_toml(str(test_toml))
assert "foo" in content.keys()
assert content["foo"] == "bar"
def test_read_toml_cannot_read(test_toml, mocker):
error = "foobar"
mocker.patch("toml.loads", side_effect=Exception(error))
with pytest.raises(MoulinetteError) as exception:
read_toml(str(test_toml))
translation = m18n.g("corrupted_toml", ressource=str(test_toml), error=error)
expected_msg = translation.format(ressource=str(test_toml), error=error)
assert expected_msg in str(exception)
def test_read_ldif(test_ldif):
dn, entry = read_ldif(str(test_ldif))[0]
assert dn == "mail=alice@example.com"
assert entry["mail"] == ["alice@example.com"]
assert entry["objectclass"] == ["top", "person"]
assert entry["cn"] == ["Alice Alison"]
dn, entry = read_ldif(str(test_ldif), ["objectclass"])[0]
assert dn == "mail=alice@example.com"
assert entry["mail"] == ["alice@example.com"]
assert "objectclass" not in entry
assert entry["cn"] == ["Alice Alison"]
def test_read_ldif_cannot_ioerror(test_ldif, mocker):
error = "foobar"
mocker.patch("__builtin__.open", side_effect=IOError(error))
with pytest.raises(MoulinetteError) as exception:
read_ldif(str(test_ldif))
translation = m18n.g("cannot_open_file", file=str(test_ldif), error=error)
expected_msg = translation.format(file=str(test_ldif), error=error)
assert expected_msg in str(exception)
def test_read_ldif_cannot_exception(test_ldif, mocker):
error = "foobar"
mocker.patch("__builtin__.open", side_effect=Exception(error))
with pytest.raises(MoulinetteError) as exception:
read_ldif(str(test_ldif))
translation = m18n.g("unknown_error_reading_file", file=str(test_ldif), error=error)
expected_msg = translation.format(file=str(test_ldif), error=error)
assert expected_msg in str(exception)
def test_write_to_existing_file(test_file):
write_to_file(str(test_file), "yolo\nswag")
assert read_file(str(test_file)) == "yolo\nswag"
@ -86,6 +183,18 @@ def test_write_to_existing_file_bad_perms(test_file, mocker):
assert expected_msg in str(exception)
def test_write_to_file_exception(test_file, mocker):
error = "foobar"
mocker.patch("__builtin__.open", side_effect=Exception(error))
with pytest.raises(MoulinetteError) as exception:
write_to_file(str(test_file), "yolo\nswag")
translation = m18n.g("error_writing_file", file=str(test_file), error=error)
expected_msg = translation.format(file=str(test_file), error=error)
assert expected_msg in str(exception)
def test_write_cannot_write_folder(tmp_path):
with pytest.raises(AssertionError):
write_to_file(str(tmp_path), "yolo\nswag")
@ -115,7 +224,7 @@ def test_append_to_new_file(tmp_path):
assert read_file(str(new_file)) == "yolo\nswag"
def text_write_dict_to_json(tmp_path):
def test_write_dict_to_json(tmp_path):
new_file = tmp_path / "newfile.json"
dummy_dict = {"foo": 42, "bar": ["a", "b", "c"]}
@ -129,6 +238,34 @@ def text_write_dict_to_json(tmp_path):
assert _json["bar"] == ["a", "b", "c"]
def test_write_json_to_existing_file_bad_perms(test_file, mocker):
error = "foobar"
dummy_dict = {"foo": 42, "bar": ["a", "b", "c"]}
mocker.patch("__builtin__.open", side_effect=IOError(error))
with pytest.raises(MoulinetteError) as exception:
write_to_json(str(test_file), dummy_dict)
translation = m18n.g("cannot_write_file", file=str(test_file), error=error)
expected_msg = translation.format(file=str(test_file), error=error)
assert expected_msg in str(exception)
def test_write_json_to_file_exception(test_file, mocker):
error = "foobar"
dummy_dict = {"foo": 42, "bar": ["a", "b", "c"]}
mocker.patch("__builtin__.open", side_effect=Exception(error))
with pytest.raises(MoulinetteError) as exception:
write_to_json(str(test_file), dummy_dict)
translation = m18n.g("error_writing_file", file=str(test_file), error=error)
expected_msg = translation.format(file=str(test_file), error=error)
assert expected_msg in str(exception)
def text_write_list_to_json(tmp_path):
new_file = tmp_path / "newfile.json"
@ -156,6 +293,218 @@ def test_write_json_cannot_write_to_non_existant_folder():
write_to_json("/toto/test.json", ["a", "b"])
def test_write_dict_to_yaml(tmp_path):
new_file = tmp_path / "newfile.yaml"
dummy_dict = {"foo": 42, "bar": ["a", "b", "c"]}
write_to_yaml(str(new_file), dummy_dict)
_yaml = read_yaml(str(new_file))
assert "foo" in _yaml.keys()
assert "bar" in _yaml.keys()
assert _yaml["foo"] == 42
assert _yaml["bar"] == ["a", "b", "c"]
def test_write_yaml_to_existing_file_bad_perms(test_file, mocker):
error = "foobar"
dummy_dict = {"foo": 42, "bar": ["a", "b", "c"]}
mocker.patch("__builtin__.open", side_effect=IOError(error))
with pytest.raises(MoulinetteError) as exception:
write_to_yaml(str(test_file), dummy_dict)
translation = m18n.g("cannot_write_file", file=str(test_file), error=error)
expected_msg = translation.format(file=str(test_file), error=error)
assert expected_msg in str(exception)
def test_write_yaml_to_file_exception(test_file, mocker):
error = "foobar"
dummy_dict = {"foo": 42, "bar": ["a", "b", "c"]}
mocker.patch("__builtin__.open", side_effect=Exception(error))
with pytest.raises(MoulinetteError) as exception:
write_to_yaml(str(test_file), dummy_dict)
translation = m18n.g("error_writing_file", file=str(test_file), error=error)
expected_msg = translation.format(file=str(test_file), error=error)
assert expected_msg in str(exception)
def text_write_list_to_yaml(tmp_path):
new_file = tmp_path / "newfile.yaml"
dummy_list = ["foo", "bar", "baz"]
write_to_yaml(str(new_file), dummy_list)
_yaml = read_yaml(str(new_file))
assert _yaml == ["foo", "bar", "baz"]
def test_write_to_yaml_bad_perms(test_yaml, mocker):
error = "foobar"
mocker.patch("__builtin__.open", side_effect=IOError(error))
with pytest.raises(MoulinetteError) as exception:
write_to_yaml(str(test_yaml), {"a": 1})
translation = m18n.g("cannot_write_file", file=str(test_yaml), error=error)
expected_msg = translation.format(file=str(test_yaml), error=error)
assert expected_msg in str(exception)
def test_write_yaml_cannot_write_to_non_existant_folder():
with pytest.raises(AssertionError):
write_to_yaml("/toto/test.yaml", ["a", "b"])
def test_mkdir(tmp_path):
new_path = tmp_path / "new_folder"
mkdir(str(new_path))
assert os.path.isdir(str(new_path))
assert oct(os.stat(str(new_path)).st_mode & 0o777) == oct(0o777)
def test_mkdir_with_permission(tmp_path, mocker):
new_path = tmp_path / "new_folder"
permission = 0o700
mkdir(str(new_path), mode=permission)
assert os.path.isdir(str(new_path))
assert oct(os.stat(str(new_path)).st_mode & 0o777) == oct(permission)
new_path = tmp_path / "new_parent2" / "new_folder"
with pytest.raises(OSError):
mkdir(str(new_path), parents=True, mode=0o000)
def test_mkdir_with_parent(tmp_path):
new_path = tmp_path / "new_folder"
mkdir(str(new_path) + "/", parents=True)
assert os.path.isdir(str(new_path))
new_path = tmp_path / "new_parent" / "new_folder"
mkdir(str(new_path), parents=True)
assert os.path.isdir(str(new_path))
def test_mkdir_existing_folder(tmp_path):
new_path = tmp_path / "new_folder"
os.makedirs(str(new_path))
with pytest.raises(OSError):
mkdir(str(new_path))
def test_chown(test_file):
with pytest.raises(ValueError):
chown(str(test_file))
current_uid = os.getuid()
current_gid = os.getgid()
chown(str(test_file), current_uid, current_gid)
assert os.stat(str(test_file)).st_uid == current_uid
assert os.stat(str(test_file)).st_gid == current_gid
current_gid = os.getgid()
chown(str(test_file), uid=None, gid=current_gid)
assert os.stat(str(test_file)).st_gid == current_gid
current_uid = pwd.getpwuid(os.getuid())[0]
current_gid = grp.getgrgid(os.getgid())[0]
chown(str(test_file), current_uid, current_gid)
assert os.stat(str(test_file)).st_uid == os.getuid()
assert os.stat(str(test_file)).st_gid == os.getgid()
fake_user = "nousrlol"
with pytest.raises(MoulinetteError) as exception:
chown(str(test_file), fake_user)
translation = m18n.g("unknown_user", user=fake_user)
expected_msg = translation.format(user=fake_user)
assert expected_msg in str(exception)
fake_grp = "nogrplol"
with pytest.raises(MoulinetteError) as exception:
chown(str(test_file), gid=fake_grp)
translation = m18n.g("unknown_group", group=fake_grp)
expected_msg = translation.format(group=fake_grp)
assert expected_msg in str(exception)
def test_chown_recursive(test_file):
current_uid = os.getuid()
dirname = os.path.dirname(str(test_file))
mkdir(os.path.join(dirname, "new_dir"))
chown(str(dirname), current_uid, recursive=True)
assert os.stat(str(dirname)).st_uid == current_uid
def test_chown_exception(test_file, mocker):
error = "foobar"
mocker.patch("os.chown", side_effect=Exception(error))
with pytest.raises(MoulinetteError) as exception:
chown(str(test_file), 1)
translation = m18n.g(
"error_changing_file_permissions", path=test_file, error=str(error)
)
expected_msg = translation.format(path=test_file, error=str(error))
assert expected_msg in str(exception)
def test_chmod(test_file):
permission = 0o723
chmod(str(test_file), permission)
assert oct(os.stat(str(test_file)).st_mode & 0o777) == oct(permission)
dirname = os.path.dirname(str(test_file))
permission = 0o722
chmod(str(dirname), permission, recursive=True)
assert oct(os.stat(str(test_file)).st_mode & 0o777) == oct(permission)
assert oct(os.stat(dirname).st_mode & 0o777) == oct(permission)
def test_chmod_recursive(test_file):
dirname = os.path.dirname(str(test_file))
mkdir(os.path.join(dirname, "new_dir"))
permission = 0o721
fpermission = 0o720
chmod(str(dirname), permission, fmode=fpermission, recursive=True)
assert oct(os.stat(str(test_file)).st_mode & 0o777) == oct(fpermission)
assert oct(os.stat(dirname).st_mode & 0o777) == oct(permission)
def test_chmod_exception(test_file, mocker):
error = "foobar"
mocker.patch("os.chmod", side_effect=Exception(error))
with pytest.raises(MoulinetteError) as exception:
chmod(str(test_file), 0o000)
translation = m18n.g(
"error_changing_file_permissions", path=test_file, error=str(error)
)
expected_msg = translation.format(path=test_file, error=str(error))
assert expected_msg in str(exception)
def test_remove_file(test_file):
assert os.path.exists(str(test_file))
rm(str(test_file))

403
test/test_ldap.py Normal file
View file

@ -0,0 +1,403 @@
import pytest
import os
from moulinette.authenticators import ldap as m_ldap
from moulinette import m18n
from moulinette.core import MoulinetteError
class TestLDAP:
def setup_method(self):
self.ldap_conf = {
"vendor": "ldap",
"name": "as-root",
"parameters": {"base_dn": "dc=yunohost,dc=org"},
"extra": {},
}
def test_authenticate_simple_bind_with_admin(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
self.ldap_conf["parameters"]["user_rdn"] = "cn=admin,dc=yunohost,dc=org"
ldap_interface = m_ldap.Authenticator(**self.ldap_conf)
ldap_interface.authenticate(password="yunohost")
assert ldap_interface.con
def test_authenticate_simple_bind_with_wrong_user(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
self.ldap_conf["parameters"]["user_rdn"] = "cn=yoloswag,dc=yunohost,dc=org"
ldap_interface = m_ldap.Authenticator(**self.ldap_conf)
with pytest.raises(MoulinetteError) as exception:
ldap_interface.authenticate(password="yunohost")
translation = m18n.g("invalid_password")
expected_msg = translation.format()
assert expected_msg in str(exception)
assert ldap_interface.con is None
def test_authenticate_simple_bind_with_rdn_wrong_password(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
self.ldap_conf["parameters"]["user_rdn"] = "cn=admin,dc=yunohost,dc=org"
ldap_interface = m_ldap.Authenticator(**self.ldap_conf)
with pytest.raises(MoulinetteError) as exception:
ldap_interface.authenticate(password="bad_password_lul")
translation = m18n.g("invalid_password")
expected_msg = translation.format()
assert expected_msg in str(exception)
assert ldap_interface.con is None
def test_authenticate_simple_bind_anonymous(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
self.ldap_conf["parameters"]["user_rdn"] = ""
ldap_interface = m_ldap.Authenticator(**self.ldap_conf)
ldap_interface.authenticate()
assert ldap_interface.con
def test_authenticate_sasl_non_interactive_bind(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
self.ldap_conf["parameters"]["user_rdn"] = (
"gidNumber=%s+uidNumber=%s,cn=peercred,cn=external,cn=auth"
% (os.getgid(), os.getuid())
)
ldap_interface = m_ldap.Authenticator(**self.ldap_conf)
assert ldap_interface.con
def test_authenticate_server_down(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
self.ldap_conf["parameters"]["user_rdn"] = "cn=admin,dc=yunohost,dc=org"
ldap_server.stop()
ldap_interface = m_ldap.Authenticator(**self.ldap_conf)
with pytest.raises(MoulinetteError) as exception:
ldap_interface.authenticate(password="yunohost")
translation = m18n.g("ldap_server_down")
expected_msg = translation.format()
assert expected_msg in str(exception)
assert ldap_interface.con is None
def create_ldap_interface(self, user_rdn, password=None):
self.ldap_conf["parameters"]["user_rdn"] = user_rdn
ldap_interface = m_ldap.Authenticator(**self.ldap_conf)
if not ldap_interface.con:
ldap_interface.authenticate(password=password)
return ldap_interface
def test_admin_read(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"cn=admin,dc=yunohost,dc=org", "yunohost"
)
admin_info = ldap_interface.search("cn=admin,dc=yunohost,dc=org", attrs=None)[0]
assert "cn" in admin_info
assert admin_info["cn"] == ["admin"]
assert "description" in admin_info
assert admin_info["description"] == ["LDAP Administrator"]
assert "userPassword" in admin_info
assert admin_info["userPassword"][0].startswith("{CRYPT}$6$")
admin_info = ldap_interface.search(
"cn=admin,dc=yunohost,dc=org", attrs=["userPassword"]
)[0]
assert admin_info.keys() == ["userPassword"]
assert admin_info["userPassword"][0].startswith("{CRYPT}$6$")
def test_sasl_read(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"gidNumber=%s+uidNumber=%s,cn=peercred,cn=external,cn=auth"
% (os.getgid(), os.getuid())
)
admin_info = ldap_interface.search("cn=admin,dc=yunohost,dc=org", attrs=None)[0]
assert "cn" in admin_info
assert admin_info["cn"] == ["admin"]
assert "description" in admin_info
assert admin_info["description"] == ["LDAP Administrator"]
assert "userPassword" in admin_info
assert admin_info["userPassword"][0].startswith("{CRYPT}$6$")
admin_info = ldap_interface.search(
"cn=admin,dc=yunohost,dc=org", attrs=["userPassword"]
)[0]
assert admin_info.keys() == ["userPassword"]
assert admin_info["userPassword"][0].startswith("{CRYPT}$6$")
def test_anonymous_read(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface("")
admin_info = ldap_interface.search("cn=admin,dc=yunohost,dc=org", attrs=None)[0]
assert "cn" in admin_info
assert admin_info["cn"] == ["admin"]
assert "description" in admin_info
assert admin_info["description"] == ["LDAP Administrator"]
assert "userPassword" not in admin_info
admin_info = ldap_interface.search(
"cn=admin,dc=yunohost,dc=org", attrs=["userPassword"]
)[0]
assert not admin_info
def add_new_user(self, ldap_interface):
new_user = "new_user"
attr_dict = {
"objectClass": ["inetOrgPerson", "posixAccount"],
"sn": new_user,
"cn": new_user,
"userPassword": new_user,
"gidNumber": "666",
"uidNumber": "666",
"homeDirectory": "/home/" + new_user,
}
ldap_interface.add("uid=%s,ou=users" % new_user, attr_dict)
# Check if we can login as the new user
assert self.create_ldap_interface(
"uid=%s,ou=users,dc=yunohost,dc=org" % new_user, new_user
).con
return ldap_interface.search(
"uid=%s,ou=users,dc=yunohost,dc=org" % new_user, attrs=None
)[0]
def test_admin_add(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"cn=admin,dc=yunohost,dc=org", "yunohost"
)
new_user_info = self.add_new_user(ldap_interface)
assert "cn" in new_user_info
assert new_user_info["cn"] == ["new_user"]
assert "sn" in new_user_info
assert new_user_info["sn"] == ["new_user"]
assert "uid" in new_user_info
assert new_user_info["uid"] == ["new_user"]
assert "objectClass" in new_user_info
assert "inetOrgPerson" in new_user_info["objectClass"]
assert "posixAccount" in new_user_info["objectClass"]
def test_sasl_add(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"gidNumber=%s+uidNumber=%s,cn=peercred,cn=external,cn=auth"
% (os.getgid(), os.getuid())
)
new_user_info = self.add_new_user(ldap_interface)
assert "cn" in new_user_info
assert new_user_info["cn"] == ["new_user"]
assert "sn" in new_user_info
assert new_user_info["sn"] == ["new_user"]
assert "uid" in new_user_info
assert new_user_info["uid"] == ["new_user"]
assert "objectClass" in new_user_info
assert "inetOrgPerson" in new_user_info["objectClass"]
assert "posixAccount" in new_user_info["objectClass"]
def test_anonymous_add(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface("")
with pytest.raises(MoulinetteError) as exception:
self.add_new_user(ldap_interface)
translation = m18n.g("ldap_operation_error", action="add")
expected_msg = translation.format(action="add")
assert expected_msg in str(exception)
def remove_new_user(self, ldap_interface):
new_user_info = self.add_new_user(
self.create_ldap_interface(
"gidNumber=%s+uidNumber=%s,cn=peercred,cn=external,cn=auth"
% (os.getgid(), os.getuid()),
"yunohost",
)
)
uid = new_user_info["uid"][0]
ldap_interface.remove("uid=%s,ou=users" % uid)
with pytest.raises(MoulinetteError) as exception:
ldap_interface.search(
"uid=%s,ou=users,dc=yunohost,dc=org" % uid, attrs=None
)
translation = m18n.g("ldap_operation_error", action="search")
expected_msg = translation.format(action="search")
assert expected_msg in str(exception)
def test_admin_remove(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"cn=admin,dc=yunohost,dc=org", "yunohost"
)
self.remove_new_user(ldap_interface)
def test_sasl_remove(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"gidNumber=%s+uidNumber=%s,cn=peercred,cn=external,cn=auth"
% (os.getgid(), os.getuid())
)
self.remove_new_user(ldap_interface)
def test_anonymous_remove(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface("")
with pytest.raises(MoulinetteError) as exception:
self.remove_new_user(ldap_interface)
translation = m18n.g("ldap_operation_error", action="remove")
expected_msg = translation.format(action="remove")
assert expected_msg in str(exception)
def update_new_user(self, ldap_interface, new_rdn=False):
new_user_info = self.add_new_user(
self.create_ldap_interface(
"gidNumber=%s+uidNumber=%s,cn=peercred,cn=external,cn=auth"
% (os.getgid(), os.getuid()),
"yunohost",
)
)
uid = new_user_info["uid"][0]
new_user_info["uidNumber"] = ["555"]
new_user_info["gidNumber"] = ["555"]
new_another_user_uid = "new_another_user"
if new_rdn:
new_rdn = "uid=%s" % new_another_user_uid
ldap_interface.update("uid=%s,ou=users" % uid, new_user_info, new_rdn)
if new_rdn:
uid = new_another_user_uid
return ldap_interface.search(
"uid=%s,ou=users,dc=yunohost,dc=org" % uid, attrs=None
)[0]
def test_admin_update(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"cn=admin,dc=yunohost,dc=org", "yunohost"
)
new_user_info = self.update_new_user(ldap_interface)
assert new_user_info["uid"] == ["new_user"]
assert new_user_info["uidNumber"] == ["555"]
assert new_user_info["gidNumber"] == ["555"]
def test_admin_update_new_rdn(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"cn=admin,dc=yunohost,dc=org", "yunohost"
)
new_user_info = self.update_new_user(ldap_interface, True)
assert new_user_info["uid"] == ["new_another_user"]
assert new_user_info["uidNumber"] == ["555"]
assert new_user_info["gidNumber"] == ["555"]
def test_sasl_update(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"gidNumber=%s+uidNumber=%s,cn=peercred,cn=external,cn=auth"
% (os.getgid(), os.getuid())
)
new_user_info = self.update_new_user(ldap_interface)
assert new_user_info["uid"] == ["new_user"]
assert new_user_info["uidNumber"] == ["555"]
assert new_user_info["gidNumber"] == ["555"]
def test_sasl_update_new_rdn(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"cn=admin,dc=yunohost,dc=org", "yunohost"
)
new_user_info = self.update_new_user(ldap_interface, True)
assert new_user_info["uid"] == ["new_another_user"]
assert new_user_info["uidNumber"] == ["555"]
assert new_user_info["gidNumber"] == ["555"]
def test_anonymous_update(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface("")
with pytest.raises(MoulinetteError) as exception:
self.update_new_user(ldap_interface)
translation = m18n.g("ldap_operation_error", action="update")
expected_msg = translation.format(action="update")
assert expected_msg in str(exception)
def test_anonymous_update_new_rdn(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface("")
with pytest.raises(MoulinetteError) as exception:
self.update_new_user(ldap_interface, True)
translation = m18n.g("ldap_operation_error", action="update")
expected_msg = translation.format(action="update")
assert expected_msg in str(exception)
def test_empty_update(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"cn=admin,dc=yunohost,dc=org", "yunohost"
)
new_user_info = self.update_new_user(ldap_interface)
assert new_user_info["uid"] == ["new_user"]
assert new_user_info["uidNumber"] == ["555"]
assert new_user_info["gidNumber"] == ["555"]
uid = new_user_info["uid"][0]
assert ldap_interface.update("uid=%s,ou=users" % uid, new_user_info)
def test_get_conflict(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"cn=admin,dc=yunohost,dc=org", "yunohost"
)
self.add_new_user(ldap_interface)
conflict = ldap_interface.get_conflict({"uid": "new_user"})
assert conflict == ("uid", "new_user")
conflict = ldap_interface.get_conflict(
{"uid": "new_user"}, base_dn="ou=users,dc=yunohost,dc=org"
)
assert conflict == ("uid", "new_user")
conflict = ldap_interface.get_conflict({"uid": "not_a_user"})
assert not conflict
def test_validate_uniqueness(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
ldap_interface = self.create_ldap_interface(
"cn=admin,dc=yunohost,dc=org", "yunohost"
)
self.add_new_user(ldap_interface)
with pytest.raises(MoulinetteError) as exception:
ldap_interface.validate_uniqueness({"uid": "new_user"})
translation = m18n.g(
"ldap_attribute_already_exists", attribute="uid", value="new_user"
)
expected_msg = translation.format(attribute="uid", value="new_user")
assert expected_msg in str(exception)
assert ldap_interface.validate_uniqueness({"uid": "not_a_user"})

View file

@ -33,9 +33,17 @@ def test_download_ssl_error(test_url):
download_text(test_url)
def test_download_connection_error(test_url):
with requests_mock.Mocker() as mock:
exception = requests.exceptions.ConnectionError
mock.register_uri("GET", test_url, exc=exception)
with pytest.raises(MoulinetteError):
download_text(test_url)
def test_download_timeout(test_url):
with requests_mock.Mocker() as mock:
exception = requests.exceptions.ConnectTimeout
exception = requests.exceptions.Timeout
mock.register_uri("GET", test_url, exc=exception)
with pytest.raises(MoulinetteError):
download_text(test_url)

View file

@ -4,6 +4,8 @@ from subprocess import CalledProcessError
import pytest
from moulinette.utils.process import run_commands
from moulinette.utils.process import call_async_output
from moulinette.utils.process import check_output
def test_run_shell_command_list(test_file):
@ -15,3 +17,104 @@ def test_run_shell_command_list(test_file):
def test_run_shell_bad_cmd():
with pytest.raises(CalledProcessError):
run_commands(["yolo swag"])
def test_run_shell_bad_cmd_with_callback():
def callback(a, b, c):
assert isinstance(a, int)
assert isinstance(b, str)
assert isinstance(c, str)
return True
assert run_commands(["yolo swag", "yolo swag", "yolo swag"], callback=callback) == 3
def callback(a, b, c):
assert isinstance(a, int)
assert isinstance(b, str)
assert isinstance(c, str)
return False
assert run_commands(["yolo swag", "yolo swag"], callback=callback) == 1
def callback(a, b, c):
assert isinstance(a, int)
assert isinstance(b, str)
assert isinstance(c, tuple)
return True
run_commands(["yolo swag"], separate_stderr=True, callback=callback)
def test_run_shell_bad_callback():
callback = 1
with pytest.raises(ValueError):
run_commands(["ls"], callback=callback)
def test_run_shell_kwargs():
with pytest.raises(ValueError):
run_commands([""], stdout="None")
with pytest.raises(ValueError):
run_commands([""], stderr="None")
run_commands(["ls"], cwd="/tmp")
with pytest.raises(OSError):
run_commands(["ls"], cwd="/yoloswag")
def test_call_async_output(test_file):
def callback(a):
assert a == "foo\n" or a == "bar\n"
call_async_output(["cat", str(test_file)], callback)
with pytest.raises(ValueError):
call_async_output(["cat", str(test_file)], 1)
def callbackA(a):
assert a == "foo\n" or a == "bar\n"
def callbackB(a):
pass
callback = (callbackA, callbackB)
call_async_output(["cat", str(test_file)], callback)
def test_call_async_output_kwargs(test_file, mocker):
def callback(a):
assert a == "foo\n" or a == "bar\n"
with pytest.raises(ValueError):
call_async_output(["cat", str(test_file)], callback, stdout=None)
with pytest.raises(ValueError):
call_async_output(["cat", str(test_file)], callback, stderr=None)
call_async_output(["cat", str(test_file)], callback, stdinfo=None)
def callbackA(a):
assert a == "foo\n" or a == "bar\n"
def callbackB(a):
pass
def callbackC(a):
pass
callback = (callbackA, callbackB, callbackC)
dirname = os.path.dirname(str(test_file))
os.mkdir(os.path.join(dirname, "teststdinfo"))
call_async_output(
["cat", str(test_file)],
callback,
stdinfo=os.path.join(dirname, "teststdinfo", "teststdinfo"),
)
def test_check_output(test_file):
assert check_output(["cat", str(test_file)], shell=False) == "foo\nbar\n"
assert check_output("cat %s" % str(test_file)) == "foo\nbar\n"

View file

@ -4,6 +4,7 @@ from moulinette.utils.text import search, searchf, prependlines, random_ascii
def test_search():
assert search("a", "a a a") == ["a", "a", "a"]
assert search("a", "a a a", count=2) == ["a", "a"]
assert search("a", "a a a", count=-1) == "a"
assert not search("a", "c c d")