diff --git a/.travis.yml b/.travis.yml index be13571e..a322499c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,11 @@ language: python +addons: + apt: + packages: + - ldap-utils + - slapd + matrix: include: - python: 3.5 diff --git a/debian/changelog b/debian/changelog index 75ad5137..2fc2ba46 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,17 @@ +moulinette (3.7.0.2) stable; urgency=low + + Bumping version number for stable release + + -- Kay0u 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 Sun, 15 Mar 2020 16:09:25 +0000 + moulinette (3.7.0) testing; urgency=low # ~ Major stuff diff --git a/locales/ar.json b/locales/ar.json index b5746321..d2853799 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -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 قيد التشغيل حاليا. في انتظار انتهاء تنفيذه قبل تشغيل التالي" } diff --git a/locales/bn_BD.json b/locales/bn_BD.json index 0967ef42..d885278e 100644 --- a/locales/bn_BD.json +++ b/locales/bn_BD.json @@ -1 +1,4 @@ -{} +{ + "logged_out": "প্রস্থান", + "password": "পাসওয়ার্ড" +} diff --git a/locales/ca.json b/locales/ca.json index 50417dcb..c0f1e040 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -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" } diff --git a/locales/cmn.json b/locales/cmn.json index bbad7315..634868a4 100644 --- a/locales/cmn.json +++ b/locales/cmn.json @@ -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": "密码", diff --git a/locales/de.json b/locales/de.json index 455eb05d..0b9f409f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -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", diff --git a/locales/el.json b/locales/el.json index 0967ef42..a6f9617e 100644 --- a/locales/el.json +++ b/locales/el.json @@ -1 +1,4 @@ -{} +{ + "logged_out": "Αποσυνδέθηκα", + "password": "Κωδικός πρόσβασης" +} diff --git a/locales/en.json b/locales/en.json index 0b55cfc5..5c55d14c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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" } diff --git a/locales/eo.json b/locales/eo.json index f61ecff2..d0f3ebd9 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -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" } diff --git a/locales/es.json b/locales/es.json index 947555bc..0e18bb58 100644 --- a/locales/es.json +++ b/locales/es.json @@ -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" } diff --git a/locales/eu.json b/locales/eu.json index db0ce305..803f875c 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -1,3 +1,6 @@ { - "argument_required": "'{argument}' argumentua beharrezkoa da" + "argument_required": "'{argument}' argumentua beharrezkoa da", + "logged_out": "Saioa amaitu", + "password": "Pasahitza", + "colon": "{}: " } diff --git a/locales/fr.json b/locales/fr.json index 732eaa0e..b0f63470 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -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" } diff --git a/locales/hu.json b/locales/hu.json index 0967ef42..42bb7781 100644 --- a/locales/hu.json +++ b/locales/hu.json @@ -1 +1,4 @@ -{} +{ + "logged_out": "Kilépett", + "password": "Jelszó" +} diff --git a/locales/it.json b/locales/it.json index 479a1b96..c7fc2864 100644 --- a/locales/it.json +++ b/locales/it.json @@ -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" } diff --git a/locales/ne.json b/locales/ne.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/locales/ne.json @@ -0,0 +1 @@ +{} diff --git a/locales/nl.json b/locales/nl.json index 54e72c97..cbbee8cc 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -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:" } diff --git a/locales/oc.json b/locales/oc.json index c7068e82..f2de095f 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -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 d’esperar que s’acabe abans de ne lançar una mai.", "invalid_argument": "Argument « {argument} » incorrècte : {error}", "invalid_password": "Senhal incorrècte", "ldap_server_down": "Impossible d’até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 qu’acabe abans d’aviar aquesta d’aquí", "warn_the_user_about_waiting_lock_again": "Encara en espèra…", - "warn_the_user_that_lock_is_acquired": "l’autra comanda ven d’acabar, lançament d’aquesta comanda" + "warn_the_user_that_lock_is_acquired": "l’autra comanda ven d’acabar, ara lançament d’aquesta comanda", + "invalid_token": "Geton invalid - volgatz vos autentificar" } diff --git a/locales/pl.json b/locales/pl.json index 0967ef42..d02a6f65 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -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" +} diff --git a/locales/pt.json b/locales/pt.json index cfc73e1e..97c96a61 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -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:" } diff --git a/locales/ru.json b/locales/ru.json index 0ef34009..03465b52 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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": "Информация:" } diff --git a/locales/sv.json b/locales/sv.json index 0967ef42..eee34d09 100644 --- a/locales/sv.json +++ b/locales/sv.json @@ -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" +} diff --git a/locales/tr.json b/locales/tr.json index ee5f4fec..7e6673e1 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -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ı açı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" } diff --git a/moulinette/__init__.py b/moulinette/__init__.py index c921604a..4aae1be0 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -33,6 +33,7 @@ __all__ = [ "api", "cli", "m18n", + "msignals", "env", "init_interface", "MoulinetteError", diff --git a/moulinette/actionsmap.py b/moulinette/actionsmap.py index 9249f32d..46d86465 100644 --- a/moulinette/actionsmap.py +++ b/moulinette/actionsmap.py @@ -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 diff --git a/moulinette/authenticators/__init__.py b/moulinette/authenticators/__init__.py index bc05a2ae..a6e51fe2 100644 --- a/moulinette/authenticators/__init__.py +++ b/moulinette/authenticators/__init__.py @@ -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): diff --git a/moulinette/authenticators/dummy.py b/moulinette/authenticators/dummy.py index d4363f75..f5b96fb7 100644 --- a/moulinette/authenticators/dummy.py +++ b/moulinette/authenticators/dummy.py @@ -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 diff --git a/moulinette/authenticators/ldap.py b/moulinette/authenticators/ldap.py index 3d27ad96..312b75f0 100644 --- a/moulinette/authenticators/ldap.py +++ b/moulinette/authenticators/ldap.py @@ -39,21 +39,23 @@ 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: - self.authenticate(None) - else: - self.con = None + if self.userdn and self.sasldn in self.userdn: + self.authenticate(None) + else: + self.con = None def __del__(self): """Disconnect and free ressources""" @@ -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 diff --git a/moulinette/core.py b/moulinette/core.py index 5467a5db..dd53ed60 100644 --- a/moulinette/core.py +++ b/moulinette/core.py @@ -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 diff --git a/moulinette/interfaces/__init__.py b/moulinette/interfaces/__init__.py index 7ec60736..2fbfe84a 100644 --- a/moulinette/interfaces/__init__.py +++ b/moulinette/interfaces/__init__.py @@ -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,8 +244,11 @@ class BaseActionsMapParser(object): elif ifaces is False: conf["authenticate"] = False elif isinstance(ifaces, list): - # Store only if authentication is needed - conf["authenticate"] = True if self.interface in ifaces else False + 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: logger.error( "expecting 'all', 'False' or a list for " @@ -256,45 +258,40 @@ class BaseActionsMapParser(object): raise MoulinetteError("error_see_log") # -- 'authenticator' - try: - auth = configuration["authenticator"] - except KeyError: - pass - else: - if not is_global and isinstance(auth, str): - try: - # Store needed authenticator profile - conf["authenticator"] = self.global_conf["authenticator"][auth] - except KeyError: - logger.error( - "requesting profile '%s' which is undefined in " - "global configuration of 'authenticator'", - auth, - ) - raise MoulinetteError("error_see_log") - elif is_global and isinstance(auth, dict): - if len(auth) == 0: - logger.warning( - "no profile defined in global configuration " - "for 'authenticator'" - ) - else: - auths = {} - for auth_name, auth_conf in auth.items(): - auths[auth_name] = { - "name": auth_name, - "vendor": auth_conf.get("vendor"), - "parameters": auth_conf.get("parameters", {}), - "extra": {"help": auth_conf.get("help", None)}, - } - conf["authenticator"] = auths - else: + auth = configuration.get("authenticator", "default") + if not is_global and isinstance(auth, str): + # Store needed authenticator profile + if auth not in self.global_conf["authenticator"]: logger.error( - "expecting a dict of profile(s) or a profile name " - "for configuration 'authenticator', got %r", + "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'" + ) + else: + auths = {} + for auth_name, auth_conf in auth.items(): + auths[auth_name] = { + "name": auth_name, + "vendor": auth_conf.get("vendor"), + "parameters": auth_conf.get("parameters", {}), + "extra": {"help": auth_conf.get("help", None)}, + } + conf["authenticator"] = auths + else: + logger.error( + "expecting a dict of profile(s) or a profile name " + "for configuration 'authenticator', got %r", + auth, + ) + raise MoulinetteError("error_see_log") return conf diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py index 72613b53..98a95c1d 100644 --- a/moulinette/interfaces/api.py +++ b/moulinette/interfaces/api.py @@ -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") + # 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: - # 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)) 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 diff --git a/moulinette/interfaces/cli.py b/moulinette/interfaces/cli.py index f4967270..21334c18 100644 --- a/moulinette/interfaces/cli.py +++ b/moulinette/interfaces/cli.py @@ -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) diff --git a/moulinette/utils/filesystem.py b/moulinette/utils/filesystem.py index 929dffa4..f453e7b6 100644 --- a/moulinette/utils/filesystem.py +++ b/moulinette/utils/filesystem.py @@ -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): diff --git a/moulinette/utils/log.py b/moulinette/utils/log.py index 58d27d11..16a3fc23 100644 --- a/moulinette/utils/log.py +++ b/moulinette/utils/log.py @@ -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"]}}, } diff --git a/moulinette/utils/network.py b/moulinette/utils/network.py index 812d02d6..8f6e60f0 100644 --- a/moulinette/utils/network.py +++ b/moulinette/utils/network.py @@ -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) diff --git a/moulinette/utils/stream.py b/moulinette/utils/stream.py index 73e62360..652565ac 100644 --- a/moulinette/utils/stream.py +++ b/moulinette/utils/stream.py @@ -43,9 +43,16 @@ class AsynchronousFileReader(Process): else: data = "" while True: - # Try to read (non-blockingly) a few bytes, append them to - # the buffer - data += os.read(self._fd, 50) + 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: diff --git a/setup.py b/setup.py index da77c38c..99611e3f 100755 --- a/setup.py +++ b/setup.py @@ -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' ], ) diff --git a/test/actionsmap/moulitest.yml b/test/actionsmap/moulitest.yml index a14bd67a..bc37b488 100644 --- a/test/actionsmap/moulitest.yml +++ b/test/actionsmap/moulitest.yml @@ -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/ + arguments: + super_arg: + help: Super Arg + + with_extra_str_only: + api: GET /test-auth/with_extra_str_only/ + 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/ + 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 diff --git a/test/conftest.py b/test/conftest.py index 1e043341..3d1079fe 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -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() diff --git a/test/ldap_files/__init__.py b/test/ldap_files/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/ldap_files/ldap_scheme.yml b/test/ldap_files/ldap_scheme.yml new file mode 100644 index 00000000..266ab714 --- /dev/null +++ b/test/ldap_files/ldap_scheme.yml @@ -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" diff --git a/test/ldap_files/schema/core.schema b/test/ldap_files/schema/core.schema new file mode 100644 index 00000000..1c92d14a --- /dev/null +++ b/test/ldap_files/schema/core.schema @@ -0,0 +1,610 @@ +# OpenLDAP Core schema +# $OpenLDAP$ +## This work is part of OpenLDAP Software . +## +## 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 +## . +# +## 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} ) + diff --git a/test/ldap_files/schema/cosine.schema b/test/ldap_files/schema/cosine.schema new file mode 100644 index 00000000..1302d837 --- /dev/null +++ b/test/ldap_files/schema/cosine.schema @@ -0,0 +1,2571 @@ +# RFC1274: Cosine and Internet X.500 schema +# $OpenLDAP$ +## This work is part of OpenLDAP Software . +## +## 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 +## . +# +# RFC1274: Cosine and Internet X.500 schema +# +# This file contains LDAPv3 schema derived from X.500 COSINE "pilot" +# schema. As this schema was defined for X.500(89), some +# oddities were introduced in the mapping to LDAPv3. The +# mappings were based upon: draft-ietf-asid-ldapv3-attributes-03.txt +# (a work in progress) +# +# Note: It seems that the pilot schema evolved beyond what was +# described in RFC1274. However, this document attempts to describes +# RFC1274 as published. +# +# Depends on core.schema + + +# Network Working Group P. Barker +# Request for Comments: 1274 S. Kille +# University College London +# November 1991 +# +# The COSINE and Internet X.500 Schema +# +# [trimmed] +# +# Abstract +# +# This document suggests an X.500 Directory Schema, or Naming +# Architecture, for use in the COSINE and Internet X.500 pilots. The +# schema is independent of any specific implementation. As well as +# indicating support for the standard object classes and attributes, a +# large number of generally useful object classes and attributes are +# also defined. An appendix to this document includes a machine +# processable version of the schema. +# +# [trimmed] + +# 7. Object Identifiers +# +# Some additional object identifiers are defined for this schema. +# These are also reproduced in Appendix C. +# +# data OBJECT IDENTIFIER ::= {ccitt 9} +# pss OBJECT IDENTIFIER ::= {data 2342} +# ucl OBJECT IDENTIFIER ::= {pss 19200300} +# pilot OBJECT IDENTIFIER ::= {ucl 100} +# +# pilotAttributeType OBJECT IDENTIFIER ::= {pilot 1} +# pilotAttributeSyntax OBJECT IDENTIFIER ::= {pilot 3} +# pilotObjectClass OBJECT IDENTIFIER ::= {pilot 4} +# pilotGroups OBJECT IDENTIFIER ::= {pilot 10} +# +# iA5StringSyntax OBJECT IDENTIFIER ::= {pilotAttributeSyntax 4} +# caseIgnoreIA5StringSyntax OBJECT IDENTIFIER ::= +# {pilotAttributeSyntax 5} +# +# 8. Object Classes +# [relocated after 9] + +# +# 9. Attribute Types +# +# 9.1. X.500 standard attribute types +# +# A number of generally useful attribute types are defined in X.520, +# and these are supported. Refer to that document for descriptions of +# the suggested usage of these attribute types. The ASN.1 for these +# attribute types is reproduced for completeness in Appendix C. +# +# 9.2. X.400 standard attribute types +# +# The standard X.400 attribute types are supported. See X.402 for full +# details. The ASN.1 for these attribute types is reproduced in +# Appendix C. +# +# 9.3. COSINE/Internet attribute types +# +# This section describes all the attribute types defined for use in the +# COSINE and Internet pilots. Descriptions are given as to the +# suggested usage of these attribute types. The ASN.1 for these +# attribute types is reproduced in Appendix C. +# +# 9.3.1. Userid +# +# The Userid attribute type specifies a computer system login name. +# +# userid ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-user-identifier)) +# ::= {pilotAttributeType 1} +# +#(in core.schema) +##attributetype ( 0.9.2342.19200300.100.1.1 NAME ( 'uid' 'userid' ) +## EQUALITY caseIgnoreMatch +## SUBSTR caseIgnoreSubstringsMatch +## SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.2. Text Encoded O/R Address +# +# The Text Encoded O/R Address attribute type specifies a text encoding +# of an X.400 O/R address, as specified in RFC 987. The use of this +# attribute is deprecated as the attribute is intended for interim use +# only. This attribute will be the first candidate for the attribute +# expiry mechanisms! +# +# textEncodedORAddress ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-text-encoded-or-address)) +# ::= {pilotAttributeType 2} +# +attributetype ( 0.9.2342.19200300.100.1.2 NAME 'textEncodedORAddress' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.3. RFC 822 Mailbox +# +# The RFC822 Mailbox attribute type specifies an electronic mailbox +# attribute following the syntax specified in RFC 822. Note that this +# attribute should not be used for greybook or other non-Internet order +# mailboxes. +# +# rfc822Mailbox ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreIA5StringSyntax +# (SIZE (1 .. ub-rfc822-mailbox)) +# ::= {pilotAttributeType 3} +# +#(in core.schema) +##attributetype ( 0.9.2342.19200300.100.1.3 NAME ( 'mail' 'rfc822Mailbox' ) +## EQUALITY caseIgnoreIA5Match +## SUBSTR caseIgnoreIA5SubstringsMatch +## SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} ) + +# 9.3.4. Information +# +# The Information attribute type specifies any general information +# pertinent to an object. It is recommended that specific usage of +# this attribute type is avoided, and that specific requirements are +# met by other (possibly additional) attribute types. +# +# info ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-information)) +# ::= {pilotAttributeType 4} +# +attributetype ( 0.9.2342.19200300.100.1.4 NAME 'info' + DESC 'RFC1274: general information' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{2048} ) + + +# 9.3.5. Favourite Drink +# +# The Favourite Drink attribute type specifies the favourite drink of +# an object (or person). +# +# favouriteDrink ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-favourite-drink)) +# ::= {pilotAttributeType 5} +# +attributetype ( 0.9.2342.19200300.100.1.5 + NAME ( 'drink' 'favouriteDrink' ) + DESC 'RFC1274: favorite drink' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.6. Room Number +# +# The Room Number attribute type specifies the room number of an +# object. Note that the commonName attribute should be used for naming +# room objects. +# +# roomNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-room-number)) +# ::= {pilotAttributeType 6} +# +attributetype ( 0.9.2342.19200300.100.1.6 NAME 'roomNumber' + DESC 'RFC1274: room number' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.7. Photo +# +# The Photo attribute type specifies a "photograph" for an object. +# This should be encoded in G3 fax as explained in recommendation T.4, +# with an ASN.1 wrapper to make it compatible with an X.400 BodyPart as +# defined in X.420. +# +# IMPORT G3FacsimileBodyPart FROM { mhs-motis ipms modules +# information-objects } +# +# photo ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# CHOICE { +# g3-facsimile [3] G3FacsimileBodyPart +# } +# (SIZE (1 .. ub-photo)) +# ::= {pilotAttributeType 7} +# +attributetype ( 0.9.2342.19200300.100.1.7 NAME 'photo' + DESC 'RFC1274: photo (G3 fax)' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.23{25000} ) + +# 9.3.8. User Class +# +# The User Class attribute type specifies a category of computer user. +# The semantics placed on this attribute are for local interpretation. +# Examples of current usage od this attribute in academia are +# undergraduate student, researcher, lecturer, etc. Note that the +# organizationalStatus attribute may now often be preferred as it makes +# no distinction between computer users and others. +# +# userClass ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-user-class)) +# ::= {pilotAttributeType 8} +# +attributetype ( 0.9.2342.19200300.100.1.8 NAME 'userClass' + DESC 'RFC1274: category of user' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.9. Host +# +# The Host attribute type specifies a host computer. +# +# host ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-host)) +# ::= {pilotAttributeType 9} +# +attributetype ( 0.9.2342.19200300.100.1.9 NAME 'host' + DESC 'RFC1274: host computer' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.10. Manager +# +# The Manager attribute type specifies the manager of an object +# represented by an entry. +# +# manager ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 10} +# +attributetype ( 0.9.2342.19200300.100.1.10 NAME 'manager' + DESC 'RFC1274: DN of manager' + EQUALITY distinguishedNameMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) + +# 9.3.11. Document Identifier +# +# The Document Identifier attribute type specifies a unique identifier +# for a document. +# +# documentIdentifier ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-document-identifier)) +# ::= {pilotAttributeType 11} +# +attributetype ( 0.9.2342.19200300.100.1.11 NAME 'documentIdentifier' + DESC 'RFC1274: unique identifier of document' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.12. Document Title +# +# The Document Title attribute type specifies the title of a document. +# +# documentTitle ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-document-title)) +# ::= {pilotAttributeType 12} +# +attributetype ( 0.9.2342.19200300.100.1.12 NAME 'documentTitle' + DESC 'RFC1274: title of document' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.13. Document Version +# +# The Document Version attribute type specifies the version number of a +# document. +# +# documentVersion ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-document-version)) +# ::= {pilotAttributeType 13} +# +attributetype ( 0.9.2342.19200300.100.1.13 NAME 'documentVersion' + DESC 'RFC1274: version of document' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.14. Document Author +# +# The Document Author attribute type specifies the distinguished name +# of the author of a document. +# +# documentAuthor ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 14} +# +attributetype ( 0.9.2342.19200300.100.1.14 NAME 'documentAuthor' + DESC 'RFC1274: DN of author of document' + EQUALITY distinguishedNameMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) + +# 9.3.15. Document Location +# +# The Document Location attribute type specifies the location of the +# document original. +# +# documentLocation ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-document-location)) +# ::= {pilotAttributeType 15} +# +attributetype ( 0.9.2342.19200300.100.1.15 NAME 'documentLocation' + DESC 'RFC1274: location of document original' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.16. Home Telephone Number +# +# The Home Telephone Number attribute type specifies a home telephone +# number associated with a person. Attribute values should follow the +# agreed format for international telephone numbers: i.e., "+44 71 123 +# 4567". +# +# homeTelephoneNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# telephoneNumberSyntax +# ::= {pilotAttributeType 20} +# +attributetype ( 0.9.2342.19200300.100.1.20 + NAME ( 'homePhone' 'homeTelephoneNumber' ) + DESC 'RFC1274: home telephone number' + EQUALITY telephoneNumberMatch + SUBSTR telephoneNumberSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 ) + +# 9.3.17. Secretary +# +# The Secretary attribute type specifies the secretary of a person. +# The attribute value for Secretary is a distinguished name. +# +# secretary ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 21} +# +attributetype ( 0.9.2342.19200300.100.1.21 NAME 'secretary' + DESC 'RFC1274: DN of secretary' + EQUALITY distinguishedNameMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) + +# 9.3.18. Other Mailbox +# +# The Other Mailbox attribute type specifies values for electronic +# mailbox types other than X.400 and rfc822. +# +# otherMailbox ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# SEQUENCE { +# mailboxType PrintableString, -- e.g. Telemail +# mailbox IA5String -- e.g. X378:Joe +# } +# ::= {pilotAttributeType 22} +# +attributetype ( 0.9.2342.19200300.100.1.22 NAME 'otherMailbox' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.39 ) + +# 9.3.19. Last Modified Time +# +# The Last Modified Time attribute type specifies the last time, in UTC +# time, that an entry was modified. Ideally, this attribute should be +# maintained by the DSA. +# +# lastModifiedTime ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# uTCTimeSyntax +# ::= {pilotAttributeType 23} +# +## Deprecated in favor of modifyTimeStamp +#attributetype ( 0.9.2342.19200300.100.1.23 NAME 'lastModifiedTime' +# DESC 'RFC1274: time of last modify, replaced by modifyTimestamp' +# OBSOLETE +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.53 +# USAGE directoryOperation ) + +# 9.3.20. Last Modified By +# +# The Last Modified By attribute specifies the distinguished name of +# the last user to modify the associated entry. Ideally, this +# attribute should be maintained by the DSA. +# +# lastModifiedBy ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 24} +# +## Deprecated in favor of modifiersName +#attributetype ( 0.9.2342.19200300.100.1.24 NAME 'lastModifiedBy' +# DESC 'RFC1274: last modifier, replaced by modifiersName' +# OBSOLETE +# EQUALITY distinguishedNameMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 +# USAGE directoryOperation ) + +# 9.3.21. Domain Component +# +# The Domain Component attribute type specifies a DNS/NRS domain. For +# example, "uk" or "ac". +# +# domainComponent ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreIA5StringSyntax +# SINGLE VALUE +# ::= {pilotAttributeType 25} +# +##(in core.schema) +##attributetype ( 0.9.2342.19200300.100.1.25 NAME ( 'dc' 'domainComponent' ) +## EQUALITY caseIgnoreIA5Match +## SUBSTR caseIgnoreIA5SubstringsMatch +## SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) + +# 9.3.22. DNS ARecord +# +# The A Record attribute type specifies a type A (Address) DNS resource +# record [6] [7]. +# +# aRecord ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# DNSRecordSyntax +# ::= {pilotAttributeType 26} +# +## incorrect syntax? +attributetype ( 0.9.2342.19200300.100.1.26 NAME 'aRecord' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +## missing from RFC1274 +## incorrect syntax? +attributetype ( 0.9.2342.19200300.100.1.27 NAME 'mDRecord' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +# 9.3.23. MX Record +# +# The MX Record attribute type specifies a type MX (Mail Exchange) DNS +# resource record [6] [7]. +# +# mXRecord ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# DNSRecordSyntax +# ::= {pilotAttributeType 28} +# +## incorrect syntax!! +attributetype ( 0.9.2342.19200300.100.1.28 NAME 'mXRecord' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +# 9.3.24. NS Record +# +# The NS Record attribute type specifies an NS (Name Server) DNS +# resource record [6] [7]. +# +# nSRecord ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# DNSRecordSyntax +# ::= {pilotAttributeType 29} +# +## incorrect syntax!! +attributetype ( 0.9.2342.19200300.100.1.29 NAME 'nSRecord' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +# 9.3.25. SOA Record +# +# The SOA Record attribute type specifies a type SOA (Start of +# Authority) DNS resorce record [6] [7]. +# +# sOARecord ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# DNSRecordSyntax +# ::= {pilotAttributeType 30} +# +## incorrect syntax!! +attributetype ( 0.9.2342.19200300.100.1.30 NAME 'sOARecord' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +# 9.3.26. CNAME Record +# +# The CNAME Record attribute type specifies a type CNAME (Canonical +# Name) DNS resource record [6] [7]. +# +# cNAMERecord ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# iA5StringSyntax +# ::= {pilotAttributeType 31} +# +## incorrect syntax!! +attributetype ( 0.9.2342.19200300.100.1.31 NAME 'cNAMERecord' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +# 9.3.27. Associated Domain +# +# The Associated Domain attribute type specifies a DNS or NRS domain +# which is associated with an object in the DIT. For example, the entry +# in the DIT with a distinguished name "C=GB, O=University College +# London" would have an associated domain of "UCL.AC.UK. Note that all +# domains should be represented in rfc822 order. See [3] for more +# details of usage of this attribute. +# +# associatedDomain ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreIA5StringSyntax +# ::= {pilotAttributeType 37} +# +#attributetype ( 0.9.2342.19200300.100.1.37 NAME 'associatedDomain' +# EQUALITY caseIgnoreIA5Match +# SUBSTR caseIgnoreIA5SubstringsMatch +# SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) + +# 9.3.28. Associated Name +# +# The Associated Name attribute type specifies an entry in the +# organisational DIT associated with a DNS/NRS domain. See [3] for +# more details of usage of this attribute. +# +# associatedName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 38} +# +attributetype ( 0.9.2342.19200300.100.1.38 NAME 'associatedName' + DESC 'RFC1274: DN of entry associated with domain' + EQUALITY distinguishedNameMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) + +# 9.3.29. Home postal address +# +# The Home postal address attribute type specifies a home postal +# address for an object. This should be limited to up to 6 lines of 30 +# characters each. +# +# homePostalAddress ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# postalAddress +# MATCHES FOR EQUALITY +# ::= {pilotAttributeType 39} +# +attributetype ( 0.9.2342.19200300.100.1.39 NAME 'homePostalAddress' + DESC 'RFC1274: home postal address' + EQUALITY caseIgnoreListMatch + SUBSTR caseIgnoreListSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 ) + +# 9.3.30. Personal Title +# +# The Personal Title attribute type specifies a personal title for a +# person. Examples of personal titles are "Ms", "Dr", "Prof" and "Rev". +# +# personalTitle ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-personal-title)) +# ::= {pilotAttributeType 40} +# +attributetype ( 0.9.2342.19200300.100.1.40 NAME 'personalTitle' + DESC 'RFC1274: personal title' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.31. Mobile Telephone Number +# +# The Mobile Telephone Number attribute type specifies a mobile +# telephone number associated with a person. Attribute values should +# follow the agreed format for international telephone numbers: i.e., +# "+44 71 123 4567". +# +# mobileTelephoneNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# telephoneNumberSyntax +# ::= {pilotAttributeType 41} +# +attributetype ( 0.9.2342.19200300.100.1.41 + NAME ( 'mobile' 'mobileTelephoneNumber' ) + DESC 'RFC1274: mobile telephone number' + EQUALITY telephoneNumberMatch + SUBSTR telephoneNumberSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 ) + +# 9.3.32. Pager Telephone Number +# +# The Pager Telephone Number attribute type specifies a pager telephone +# number for an object. Attribute values should follow the agreed +# format for international telephone numbers: i.e., "+44 71 123 4567". +# +# pagerTelephoneNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# telephoneNumberSyntax +# ::= {pilotAttributeType 42} +# +attributetype ( 0.9.2342.19200300.100.1.42 + NAME ( 'pager' 'pagerTelephoneNumber' ) + DESC 'RFC1274: pager telephone number' + EQUALITY telephoneNumberMatch + SUBSTR telephoneNumberSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 ) + +# 9.3.33. Friendly Country Name +# +# The Friendly Country Name attribute type specifies names of countries +# in human readable format. The standard attribute country name must +# be one of the two-letter codes defined in ISO 3166. +# +# friendlyCountryName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# ::= {pilotAttributeType 43} +# +attributetype ( 0.9.2342.19200300.100.1.43 + NAME ( 'co' 'friendlyCountryName' ) + DESC 'RFC1274: friendly country name' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) + +# 9.3.34. Unique Identifier +# +# The Unique Identifier attribute type specifies a "unique identifier" +# for an object represented in the Directory. The domain within which +# the identifier is unique, and the exact semantics of the identifier, +# are for local definition. For a person, this might be an +# institution-wide payroll number. For an organisational unit, it +# might be a department code. +# +# uniqueIdentifier ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-unique-identifier)) +# ::= {pilotAttributeType 44} +# +attributetype ( 0.9.2342.19200300.100.1.44 NAME 'uniqueIdentifier' + DESC 'RFC1274: unique identifer' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.35. Organisational Status +# +# The Organisational Status attribute type specifies a category by +# which a person is often referred to in an organisation. Examples of +# usage in academia might include undergraduate student, researcher, +# lecturer, etc. +# +# A Directory administrator should probably consider carefully the +# distinctions between this and the title and userClass attributes. +# +# organizationalStatus ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-organizational-status)) +# ::= {pilotAttributeType 45} +# +attributetype ( 0.9.2342.19200300.100.1.45 NAME 'organizationalStatus' + DESC 'RFC1274: organizational status' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.36. Janet Mailbox +# +# The Janet Mailbox attribute type specifies an electronic mailbox +# attribute following the syntax specified in the Grey Book of the +# Coloured Book series. This attribute is intended for the convenience +# of U.K users unfamiliar with rfc822 and little-endian mail addresses. +# Entries using this attribute MUST also include an rfc822Mailbox +# attribute. +# +# janetMailbox ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreIA5StringSyntax +# (SIZE (1 .. ub-janet-mailbox)) +# ::= {pilotAttributeType 46} +# +attributetype ( 0.9.2342.19200300.100.1.46 NAME 'janetMailbox' + DESC 'RFC1274: Janet mailbox' + EQUALITY caseIgnoreIA5Match + SUBSTR caseIgnoreIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} ) + +# 9.3.37. Mail Preference Option +# +# An attribute to allow users to indicate a preference for inclusion of +# their names on mailing lists (electronic or physical). The absence +# of such an attribute should be interpreted as if the attribute was +# present with value "no-list-inclusion". This attribute should be +# interpreted by anyone using the directory to derive mailing lists, +# and its value respected. +# +# mailPreferenceOption ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX ENUMERATED { +# no-list-inclusion(0), +# any-list-inclusion(1), -- may be added to any lists +# professional-list-inclusion(2) +# -- may be added to lists +# -- which the list provider +# -- views as related to the +# -- users professional inter- +# -- ests, perhaps evaluated +# -- from the business of the +# -- organisation or keywords +# -- in the entry. +# } +# ::= {pilotAttributeType 47} +# +attributetype ( 0.9.2342.19200300.100.1.47 + NAME 'mailPreferenceOption' + DESC 'RFC1274: mail preference option' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 ) + +# 9.3.38. Building Name +# +# The Building Name attribute type specifies the name of the building +# where an organisation or organisational unit is based. +# +# buildingName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-building-name)) +# ::= {pilotAttributeType 48} +# +attributetype ( 0.9.2342.19200300.100.1.48 NAME 'buildingName' + DESC 'RFC1274: name of building' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +# 9.3.39. DSA Quality +# +# The DSA Quality attribute type specifies the purported quality of a +# DSA. It allows a DSA manager to indicate the expected level of +# availability of the DSA. See [8] for details of the syntax. +# +# dSAQuality ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX DSAQualitySyntax +# SINGLE VALUE +# ::= {pilotAttributeType 49} +# +attributetype ( 0.9.2342.19200300.100.1.49 NAME 'dSAQuality' + DESC 'RFC1274: DSA Quality' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.19 SINGLE-VALUE ) + +# 9.3.40. Single Level Quality +# +# The Single Level Quality attribute type specifies the purported data +# quality at the level immediately below in the DIT. See [8] for +# details of the syntax. +# +# singleLevelQuality ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX DataQualitySyntax +# SINGLE VALUE +# ::= {pilotAttributeType 50} +# +attributetype ( 0.9.2342.19200300.100.1.50 NAME 'singleLevelQuality' + DESC 'RFC1274: Single Level Quality' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE ) + +# 9.3.41. Subtree Minimum Quality +# +# The Subtree Minimum Quality attribute type specifies the purported +# minimum data quality for a DIT subtree. See [8] for more discussion +# and details of the syntax. +# +# subtreeMinimumQuality ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX DataQualitySyntax +# SINGLE VALUE +# -- Defaults to singleLevelQuality +# ::= {pilotAttributeType 51} +# +attributetype ( 0.9.2342.19200300.100.1.51 NAME 'subtreeMinimumQuality' + DESC 'RFC1274: Subtree Mininum Quality' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE ) + +# 9.3.42. Subtree Maximum Quality +# +# The Subtree Maximum Quality attribute type specifies the purported +# maximum data quality for a DIT subtree. See [8] for more discussion +# and details of the syntax. +# +# subtreeMaximumQuality ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX DataQualitySyntax +# SINGLE VALUE +# -- Defaults to singleLevelQuality +# ::= {pilotAttributeType 52} +# +attributetype ( 0.9.2342.19200300.100.1.52 NAME 'subtreeMaximumQuality' + DESC 'RFC1274: Subtree Maximun Quality' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE ) + +# 9.3.43. Personal Signature +# +# The Personal Signature attribute type allows for a representation of +# a person's signature. This should be encoded in G3 fax as explained +# in recommendation T.4, with an ASN.1 wrapper to make it compatible +# with an X.400 BodyPart as defined in X.420. +# +# IMPORT G3FacsimileBodyPart FROM { mhs-motis ipms modules +# information-objects } +# +# personalSignature ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# CHOICE { +# g3-facsimile [3] G3FacsimileBodyPart +# } +# (SIZE (1 .. ub-personal-signature)) +# ::= {pilotAttributeType 53} +# +attributetype ( 0.9.2342.19200300.100.1.53 NAME 'personalSignature' + DESC 'RFC1274: Personal Signature (G3 fax)' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.23 ) + +# 9.3.44. DIT Redirect +# +# The DIT Redirect attribute type is used to indicate that the object +# described by one entry now has a newer entry in the DIT. The entry +# containing the redirection attribute should be expired after a +# suitable grace period. This attribute may be used when an individual +# changes his/her place of work, and thus acquires a new organisational +# DN. +# +# dITRedirect ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 54} +# +attributetype ( 0.9.2342.19200300.100.1.54 NAME 'dITRedirect' + DESC 'RFC1274: DIT Redirect' + EQUALITY distinguishedNameMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) + +# 9.3.45. Audio +# +# The Audio attribute type allows the storing of sounds in the +# Directory. The attribute uses a u-law encoded sound file as used by +# the "play" utility on a Sun 4. This is an interim format. +# +# audio ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# Audio +# (SIZE (1 .. ub-audio)) +# ::= {pilotAttributeType 55} +# +attributetype ( 0.9.2342.19200300.100.1.55 NAME 'audio' + DESC 'RFC1274: audio (u-law)' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.4{25000} ) + +# 9.3.46. Publisher of Document +# +# +# The Publisher of Document attribute is the person and/or organization +# that published a document. +# +# documentPublisher ATTRIBUTE +# WITH ATTRIBUTE SYNTAX caseIgnoreStringSyntax +# ::= {pilotAttributeType 56} +# +attributetype ( 0.9.2342.19200300.100.1.56 NAME 'documentPublisher' + DESC 'RFC1274: publisher of document' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) + +# 9.4. Generally useful syntaxes +# +# caseIgnoreIA5StringSyntax ATTRIBUTE-SYNTAX +# IA5String +# MATCHES FOR EQUALITY SUBSTRINGS +# +# iA5StringSyntax ATTRIBUTE-SYNTAX +# IA5String +# MATCHES FOR EQUALITY SUBSTRINGS +# +# +# -- Syntaxes to support the DNS attributes +# +# DNSRecordSyntax ATTRIBUTE-SYNTAX +# IA5String +# MATCHES FOR EQUALITY +# +# +# NRSInformationSyntax ATTRIBUTE-SYNTAX +# NRSInformation +# MATCHES FOR EQUALITY +# +# +# NRSInformation ::= SET { +# [0] Context, +# [1] Address-space-id, +# routes [2] SEQUENCE OF SEQUENCE { +# Route-cost, +# Addressing-info } +# } +# +# +# 9.5. Upper bounds on length of attribute values +# +# +# ub-document-identifier INTEGER ::= 256 +# +# ub-document-location INTEGER ::= 256 +# +# ub-document-title INTEGER ::= 256 +# +# ub-document-version INTEGER ::= 256 +# +# ub-favourite-drink INTEGER ::= 256 +# +# ub-host INTEGER ::= 256 +# +# ub-information INTEGER ::= 2048 +# +# ub-unique-identifier INTEGER ::= 256 +# +# ub-personal-title INTEGER ::= 256 +# +# ub-photo INTEGER ::= 250000 +# +# ub-rfc822-mailbox INTEGER ::= 256 +# +# ub-room-number INTEGER ::= 256 +# +# ub-text-or-address INTEGER ::= 256 +# +# ub-user-class INTEGER ::= 256 +# +# ub-user-identifier INTEGER ::= 256 +# +# ub-organizational-status INTEGER ::= 256 +# +# ub-janet-mailbox INTEGER ::= 256 +# +# ub-building-name INTEGER ::= 256 +# +# ub-personal-signature ::= 50000 +# +# ub-audio INTEGER ::= 250000 +# + +# [back to 8] +# 8. Object Classes +# +# 8.1. X.500 standard object classes +# +# A number of generally useful object classes are defined in X.521, and +# these are supported. Refer to that document for descriptions of the +# suggested usage of these object classes. The ASN.1 for these object +# classes is reproduced for completeness in Appendix C. +# +# 8.2. X.400 standard object classes +# +# A number of object classes defined in X.400 are supported. Refer to +# X.402 for descriptions of the usage of these object classes. The +# ASN.1 for these object classes is reproduced for completeness in +# Appendix C. +# +# 8.3. COSINE/Internet object classes +# +# This section attempts to fuse together the object classes designed +# for use in the COSINE and Internet pilot activities. Descriptions +# are given of the suggested usage of these object classes. The ASN.1 +# for these object classes is also reproduced in Appendix C. +# +# 8.3.1. Pilot Object +# +# The PilotObject object class is used as a sub-class to allow some +# common, useful attributes to be assigned to entries of all other +# object classes. +# +# pilotObject OBJECT-CLASS +# SUBCLASS OF top +# MAY CONTAIN { +# info, +# photo, +# manager, +# uniqueIdentifier, +# lastModifiedTime, +# lastModifiedBy, +# dITRedirect, +# audio} +# ::= {pilotObjectClass 3} +# +#objectclass ( 0.9.2342.19200300.100.4.3 NAME 'pilotObject' +# DESC 'RFC1274: pilot object' +# SUP top AUXILIARY +# MAY ( info $ photo $ manager $ uniqueIdentifier $ +# lastModifiedTime $ lastModifiedBy $ dITRedirect $ audio ) +# ) + +# 8.3.2. Pilot Person +# +# The PilotPerson object class is used as a sub-class of person, to +# allow the use of a number of additional attributes to be assigned to +# entries of object class person. +# +# pilotPerson OBJECT-CLASS +# SUBCLASS OF person +# MAY CONTAIN { +# userid, +# textEncodedORAddress, +# rfc822Mailbox, +# favouriteDrink, +# roomNumber, +# userClass, +# homeTelephoneNumber, +# homePostalAddress, +# secretary, +# personalTitle, +# preferredDeliveryMethod, +# businessCategory, +# janetMailbox, +# otherMailbox, +# mobileTelephoneNumber, +# pagerTelephoneNumber, +# organizationalStatus, +# mailPreferenceOption, +# personalSignature} +# ::= {pilotObjectClass 4} +# +objectclass ( 0.9.2342.19200300.100.4.4 + NAME ( 'pilotPerson' 'newPilotPerson' ) + SUP person STRUCTURAL + MAY ( userid $ textEncodedORAddress $ rfc822Mailbox $ + favouriteDrink $ roomNumber $ userClass $ + homeTelephoneNumber $ homePostalAddress $ secretary $ + personalTitle $ preferredDeliveryMethod $ businessCategory $ + janetMailbox $ otherMailbox $ mobileTelephoneNumber $ + pagerTelephoneNumber $ organizationalStatus $ + mailPreferenceOption $ personalSignature ) + ) + +# 8.3.3. Account +# +# The Account object class is used to define entries representing +# computer accounts. The userid attribute should be used for naming +# entries of this object class. +# +# account OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# userid} +# MAY CONTAIN { +# description, +# seeAlso, +# localityName, +# organizationName, +# organizationalUnitName, +# host} +# ::= {pilotObjectClass 5} +# +objectclass ( 0.9.2342.19200300.100.4.5 NAME 'account' + SUP top STRUCTURAL + MUST userid + MAY ( description $ seeAlso $ localityName $ + organizationName $ organizationalUnitName $ host ) + ) + +# 8.3.4. Document +# +# The Document object class is used to define entries which represent +# documents. +# +# document OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# documentIdentifier} +# MAY CONTAIN { +# commonName, +# description, +# seeAlso, +# localityName, +# organizationName, +# organizationalUnitName, +# documentTitle, +# documentVersion, +# documentAuthor, +# documentLocation, +# documentPublisher} +# ::= {pilotObjectClass 6} +# +objectclass ( 0.9.2342.19200300.100.4.6 NAME 'document' + SUP top STRUCTURAL + MUST documentIdentifier + MAY ( commonName $ description $ seeAlso $ localityName $ + organizationName $ organizationalUnitName $ + documentTitle $ documentVersion $ documentAuthor $ + documentLocation $ documentPublisher ) + ) + +# 8.3.5. Room +# +# The Room object class is used to define entries representing rooms. +# The commonName attribute should be used for naming pentries of this +# object class. +# +# room OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# commonName} +# MAY CONTAIN { +# roomNumber, +# description, +# seeAlso, +# telephoneNumber} +# ::= {pilotObjectClass 7} +# +objectclass ( 0.9.2342.19200300.100.4.7 NAME 'room' + SUP top STRUCTURAL + MUST commonName + MAY ( roomNumber $ description $ seeAlso $ telephoneNumber ) + ) + +# 8.3.6. Document Series +# +# The Document Series object class is used to define an entry which +# represents a series of documents (e.g., The Request For Comments +# papers). +# +# documentSeries OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# commonName} +# MAY CONTAIN { +# description, +# seeAlso, +# telephoneNumber, +# localityName, +# organizationName, +# organizationalUnitName} +# ::= {pilotObjectClass 9} +# +objectclass ( 0.9.2342.19200300.100.4.9 NAME 'documentSeries' + SUP top STRUCTURAL + MUST commonName + MAY ( description $ seeAlso $ telephonenumber $ + localityName $ organizationName $ organizationalUnitName ) + ) + +# 8.3.7. Domain +# +# The Domain object class is used to define entries which represent DNS +# or NRS domains. The domainComponent attribute should be used for +# naming entries of this object class. The usage of this object class +# is described in more detail in [3]. +# +# domain OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# domainComponent} +# MAY CONTAIN { +# associatedName, +# organizationName, +# organizationalAttributeSet} +# ::= {pilotObjectClass 13} +# +objectclass ( 0.9.2342.19200300.100.4.13 NAME 'domain' + SUP top STRUCTURAL + MUST domainComponent + MAY ( associatedName $ organizationName $ description $ + businessCategory $ seeAlso $ searchGuide $ userPassword $ + localityName $ stateOrProvinceName $ streetAddress $ + physicalDeliveryOfficeName $ postalAddress $ postalCode $ + postOfficeBox $ streetAddress $ + facsimileTelephoneNumber $ internationalISDNNumber $ + telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ + preferredDeliveryMethod $ destinationIndicator $ + registeredAddress $ x121Address ) + ) + +# 8.3.8. RFC822 Local Part +# +# The RFC822 Local Part object class is used to define entries which +# represent the local part of RFC822 mail addresses. This treats this +# part of an RFC822 address as a domain. The usage of this object +# class is described in more detail in [3]. +# +# rFC822localPart OBJECT-CLASS +# SUBCLASS OF domain +# MAY CONTAIN { +# commonName, +# surname, +# description, +# seeAlso, +# telephoneNumber, +# postalAttributeSet, +# telecommunicationAttributeSet} +# ::= {pilotObjectClass 14} +# +objectclass ( 0.9.2342.19200300.100.4.14 NAME 'RFC822localPart' + SUP domain STRUCTURAL + MAY ( commonName $ surname $ description $ seeAlso $ telephoneNumber $ + physicalDeliveryOfficeName $ postalAddress $ postalCode $ + postOfficeBox $ streetAddress $ + facsimileTelephoneNumber $ internationalISDNNumber $ + telephoneNumber $ teletexTerminalIdentifier $ + telexNumber $ preferredDeliveryMethod $ destinationIndicator $ + registeredAddress $ x121Address ) + ) + +# 8.3.9. DNS Domain +# +# The DNS Domain (Domain NameServer) object class is used to define +# entries for DNS domains. The usage of this object class is described +# in more detail in [3]. +# +# dNSDomain OBJECT-CLASS +# SUBCLASS OF domain +# MAY CONTAIN { +# ARecord, +# MDRecord, +# MXRecord, +# NSRecord, +# SOARecord, +# CNAMERecord} +# ::= {pilotObjectClass 15} +# +objectclass ( 0.9.2342.19200300.100.4.15 NAME 'dNSDomain' + SUP domain STRUCTURAL + MAY ( ARecord $ MDRecord $ MXRecord $ NSRecord $ + SOARecord $ CNAMERecord ) + ) + +# 8.3.10. Domain Related Object +# +# The Domain Related Object object class is used to define entries +# which represent DNS/NRS domains which are "equivalent" to an X.500 +# domain: e.g., an organisation or organisational unit. The usage of +# this object class is described in more detail in [3]. +# +# domainRelatedObject OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# associatedDomain} +# ::= {pilotObjectClass 17} +# +objectclass ( 0.9.2342.19200300.100.4.17 NAME 'domainRelatedObject' + DESC 'RFC1274: an object related to an domain' + SUP top AUXILIARY + MUST associatedDomain ) + +# 8.3.11. Friendly Country +# +# The Friendly Country object class is used to define country entries +# in the DIT. The object class is used to allow friendlier naming of +# countries than that allowed by the object class country. The naming +# attribute of object class country, countryName, has to be a 2 letter +# string defined in ISO 3166. +# +# friendlyCountry OBJECT-CLASS +# SUBCLASS OF country +# MUST CONTAIN { +# friendlyCountryName} +# ::= {pilotObjectClass 18} +# +objectclass ( 0.9.2342.19200300.100.4.18 NAME 'friendlyCountry' + SUP country STRUCTURAL + MUST friendlyCountryName ) + +# 8.3.12. Simple Security Object +# +# The Simple Security Object object class is used to allow an entry to +# have a userPassword attribute when an entry's principal object +# classes do not allow userPassword as an attribute type. +# +# simpleSecurityObject OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# userPassword } +# ::= {pilotObjectClass 19} +# +## (in core.schema) +## objectclass ( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' +## SUP top AUXILIARY +## MUST userPassword ) + +# 8.3.13. Pilot Organization +# +# The PilotOrganization object class is used as a sub-class of +# organization and organizationalUnit to allow a number of additional +# attributes to be assigned to entries of object classes organization +# and organizationalUnit. +# +# pilotOrganization OBJECT-CLASS +# SUBCLASS OF organization, organizationalUnit +# MAY CONTAIN { +# buildingName} +# ::= {pilotObjectClass 20} +# +objectclass ( 0.9.2342.19200300.100.4.20 NAME 'pilotOrganization' + SUP ( organization $ organizationalUnit ) STRUCTURAL + MAY buildingName ) + +# 8.3.14. Pilot DSA +# +# The PilotDSA object class is used as a sub-class of the dsa object +# class to allow additional attributes to be assigned to entries for +# DSAs. +# +# pilotDSA OBJECT-CLASS +# SUBCLASS OF dsa +# MUST CONTAIN { +# dSAQuality} +# ::= {pilotObjectClass 21} +# +objectclass ( 0.9.2342.19200300.100.4.21 NAME 'pilotDSA' + SUP dsa STRUCTURAL + MAY dSAQuality ) + +# 8.3.15. Quality Labelled Data +# +# The Quality Labelled Data object class is used to allow the +# assignment of the data quality attributes to subtrees in the DIT. +# +# See [8] for more details. +# +# qualityLabelledData OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# dSAQuality} +# MAY CONTAIN { +# subtreeMinimumQuality, +# subtreeMaximumQuality} +# ::= {pilotObjectClass 22} +objectclass ( 0.9.2342.19200300.100.4.22 NAME 'qualityLabelledData' + SUP top AUXILIARY + MUST dsaQuality + MAY ( subtreeMinimumQuality $ subtreeMaximumQuality ) + ) + + +# References +# +# [1] CCITT/ISO, "X.500, The Directory - overview of concepts, +# models and services, CCITT /ISO IS 9594. +# +# [2] Kille, S., "The THORN and RARE X.500 Naming Architecture, in +# University College London, Department of Computer Science +# Research Note 89/48, May 1989. +# +# [3] Kille, S., "X.500 and Domains", RFC 1279, University College +# London, November 1991. +# +# [4] Rose, M., "PSI/NYSERNet White Pages Pilot Project: Status +# Report", Technical Report 90-09-10-1, published by NYSERNet +# Inc, 1990. +# +# [5] Craigie, J., "UK Academic Community Directory Service Pilot +# Project, pp. 305-310 in Computer Networks and ISDN Systems +# 17 (1989), published by North Holland. +# +# [6] Mockapetris, P., "Domain Names - Concepts and Facilities", +# RFC 1034, USC/Information Sciences Institute, November 1987. +# +# [7] Mockapetris, P., "Domain Names - Implementation and +# Specification, RFC 1035, USC/Information Sciences Institute, +# November 1987. +# +# [8] Kille, S., "Handling QOS (Quality of service) in the +# Directory," publication in process, March 1991. +# +# +# APPENDIX C - Summary of all Object Classes and Attribute Types +# +# -- Some Important Object Identifiers +# +# data OBJECT IDENTIFIER ::= {ccitt 9} +# pss OBJECT IDENTIFIER ::= {data 2342} +# ucl OBJECT IDENTIFIER ::= {pss 19200300} +# pilot OBJECT IDENTIFIER ::= {ucl 100} +# +# pilotAttributeType OBJECT IDENTIFIER ::= {pilot 1} +# pilotAttributeSyntax OBJECT IDENTIFIER ::= {pilot 3} +# pilotObjectClass OBJECT IDENTIFIER ::= {pilot 4} +# pilotGroups OBJECT IDENTIFIER ::= {pilot 10} +# +# iA5StringSyntax OBJECT IDENTIFIER ::= {pilotAttributeSyntax 4} +# caseIgnoreIA5StringSyntax OBJECT IDENTIFIER ::= +# {pilotAttributeSyntax 5} +# +# -- Standard Object Classes +# +# top OBJECT-CLASS +# MUST CONTAIN { +# objectClass} +# ::= {objectClass 0} +# +# +# alias OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# aliasedObjectName} +# ::= {objectClass 1} +# +# +# country OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# countryName} +# MAY CONTAIN { +# description, +# searchGuide} +# ::= {objectClass 2} +# +# +# locality OBJECT-CLASS +# SUBCLASS OF top +# MAY CONTAIN { +# description, +# localityName, +# stateOrProvinceName, +# searchGuide, +# seeAlso, +# streetAddress} +# ::= {objectClass 3} +# +# +# organization OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# organizationName} +# MAY CONTAIN { +# organizationalAttributeSet} +# ::= {objectClass 4} +# +# +# organizationalUnit OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# organizationalUnitName} +# MAY CONTAIN { +# organizationalAttributeSet} +# ::= {objectClass 5} +# +# +# person OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# commonName, +# surname} +# MAY CONTAIN { +# description, +# seeAlso, +# telephoneNumber, +# userPassword} +# ::= {objectClass 6} +# +# +# organizationalPerson OBJECT-CLASS +# SUBCLASS OF person +# MAY CONTAIN { +# localeAttributeSet, +# organizationalUnitName, +# postalAttributeSet, +# telecommunicationAttributeSet, +# title} +# ::= {objectClass 7} +# +# +# organizationalRole OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# commonName} +# MAY CONTAIN { +# description, +# localeAttributeSet, +# organizationalUnitName, +# postalAttributeSet, +# preferredDeliveryMethod, +# roleOccupant, +# seeAlso, +# telecommunicationAttributeSet} +# ::= {objectClass 8} +# +# +# groupOfNames OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# commonName, +# member} +# MAY CONTAIN { +# description, +# organizationName, +# organizationalUnitName, +# owner, +# seeAlso, +# businessCategory} +# ::= {objectClass 9} +# +# +# residentialPerson OBJECT-CLASS +# SUBCLASS OF person +# MUST CONTAIN { +# localityName} +# MAY CONTAIN { +# localeAttributeSet, +# postalAttributeSet, +# preferredDeliveryMethod, +# telecommunicationAttributeSet, +# businessCategory} +# ::= {objectClass 10} +# +# +# applicationProcess OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# commonName} +# MAY CONTAIN { +# description, +# localityName, +# organizationalUnitName, +# seeAlso} +# ::= {objectClass 11} +# +# +# applicationEntity OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# commonName, +# presentationAddress} +# MAY CONTAIN { +# description, +# localityName, +# organizationName, +# organizationalUnitName, +# seeAlso, +# supportedApplicationContext} +# ::= {objectClass 12} +# +# +# dSA OBJECT-CLASS +# SUBCLASS OF applicationEntity +# MAY CONTAIN { +# knowledgeInformation} +# ::= {objectClass 13} +# +# +# device OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# commonName} +# MAY CONTAIN { +# description, +# localityName, +# organizationName, +# organizationalUnitName, +# owner, +# seeAlso, +# serialNumber} +# ::= {objectClass 14} +# +# +# strongAuthenticationUser OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# userCertificate} +# ::= {objectClass 15} +# +# +# certificationAuthority OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# cACertificate, +# certificateRevocationList, +# authorityRevocationList} +# MAY CONTAIN { +# crossCertificatePair} +# ::= {objectClass 16} +# +# -- Standard MHS Object Classes +# +# mhsDistributionList OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# commonName, +# mhsDLSubmitPermissions, +# mhsORAddresses} +# MAY CONTAIN { +# description, +# organizationName, +# organizationalUnitName, +# owner, +# seeAlso, +# mhsDeliverableContentTypes, +# mhsdeliverableEits, +# mhsDLMembers, +# mhsPreferredDeliveryMethods} +# ::= {mhsObjectClass 0} +# +# +# mhsMessageStore OBJECT-CLASS +# SUBCLASS OF applicationEntity +# MAY CONTAIN { +# description, +# owner, +# mhsSupportedOptionalAttributes, +# mhsSupportedAutomaticActions, +# mhsSupportedContentTypes} +# ::= {mhsObjectClass 1} +# +# +# mhsMessageTransferAgent OBJECT-CLASS +# SUBCLASS OF applicationEntity +# MAY CONTAIN { +# description, +# owner, +# mhsDeliverableContentLength} +# ::= {mhsObjectClass 2} +# +# +# mhsOrganizationalUser OBJECT-CLASS +# SUBCLASS OF organizationalPerson +# MUST CONTAIN { +# mhsORAddresses} +# MAY CONTAIN { +# mhsDeliverableContentLength, +# mhsDeliverableContentTypes, +# mhsDeliverableEits, +# mhsMessageStoreName, +# mhsPreferredDeliveryMethods } +# ::= {mhsObjectClass 3} +# +# +# mhsResidentialUser OBJECT-CLASS +# SUBCLASS OF residentialPerson +# MUST CONTAIN { +# mhsORAddresses} +# MAY CONTAIN { +# mhsDeliverableContentLength, +# mhsDeliverableContentTypes, +# mhsDeliverableEits, +# mhsMessageStoreName, +# mhsPreferredDeliveryMethods } +# ::= {mhsObjectClass 4} +# +# +# mhsUserAgent OBJECT-CLASS +# SUBCLASS OF applicationEntity +# MAY CONTAIN { +# mhsDeliverableContentLength, +# mhsDeliverableContentTypes, +# mhsDeliverableEits, +# mhsORAddresses, +# owner} +# ::= {mhsObjectClass 5} +# +# +# +# +# -- Pilot Object Classes +# +# pilotObject OBJECT-CLASS +# SUBCLASS OF top +# MAY CONTAIN { +# info, +# photo, +# manager, +# uniqueIdentifier, +# lastModifiedTime, +# lastModifiedBy, +# dITRedirect, +# audio} +# ::= {pilotObjectClass 3} +# pilotPerson OBJECT-CLASS +# SUBCLASS OF person +# MAY CONTAIN { +# userid, +# textEncodedORAddress, +# rfc822Mailbox, +# favouriteDrink, +# roomNumber, +# userClass, +# homeTelephoneNumber, +# homePostalAddress, +# secretary, +# personalTitle, +# preferredDeliveryMethod, +# businessCategory, +# janetMailbox, +# otherMailbox, +# mobileTelephoneNumber, +# pagerTelephoneNumber, +# organizationalStatus, +# mailPreferenceOption, +# personalSignature} +# ::= {pilotObjectClass 4} +# +# +# account OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# userid} +# MAY CONTAIN { +# description, +# seeAlso, +# localityName, +# organizationName, +# organizationalUnitName, +# host} +# ::= {pilotObjectClass 5} +# +# +# document OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# documentIdentifier} +# MAY CONTAIN { +# commonName, +# description, +# seeAlso, +# localityName, +# organizationName, +# organizationalUnitName, +# documentTitle, +# documentVersion, +# documentAuthor, +# documentLocation, +# documentPublisher} +# ::= {pilotObjectClass 6} +# +# +# room OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# commonName} +# MAY CONTAIN { +# roomNumber, +# description, +# seeAlso, +# telephoneNumber} +# ::= {pilotObjectClass 7} +# +# +# documentSeries OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# commonName} +# MAY CONTAIN { +# description, +# seeAlso, +# telephoneNumber, +# localityName, +# organizationName, +# organizationalUnitName} +# ::= {pilotObjectClass 9} +# +# +# domain OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# domainComponent} +# MAY CONTAIN { +# associatedName, +# organizationName, +# organizationalAttributeSet} +# ::= {pilotObjectClass 13} +# +# +# rFC822localPart OBJECT-CLASS +# SUBCLASS OF domain +# MAY CONTAIN { +# commonName, +# surname, +# description, +# seeAlso, +# telephoneNumber, +# postalAttributeSet, +# telecommunicationAttributeSet} +# ::= {pilotObjectClass 14} +# +# +# dNSDomain OBJECT-CLASS +# SUBCLASS OF domain +# MAY CONTAIN { +# ARecord, +# MDRecord, +# MXRecord, +# NSRecord, +# SOARecord, +# CNAMERecord} +# ::= {pilotObjectClass 15} +# +# +# domainRelatedObject OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# associatedDomain} +# ::= {pilotObjectClass 17} +# +# +# friendlyCountry OBJECT-CLASS +# SUBCLASS OF country +# MUST CONTAIN { +# friendlyCountryName} +# ::= {pilotObjectClass 18} +# +# +# simpleSecurityObject OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# userPassword } +# ::= {pilotObjectClass 19} +# +# +# pilotOrganization OBJECT-CLASS +# SUBCLASS OF organization, organizationalUnit +# MAY CONTAIN { +# buildingName} +# ::= {pilotObjectClass 20} +# +# +# pilotDSA OBJECT-CLASS +# SUBCLASS OF dsa +# MUST CONTAIN { +# dSAQuality} +# ::= {pilotObjectClass 21} +# +# +# qualityLabelledData OBJECT-CLASS +# SUBCLASS OF top +# MUST CONTAIN { +# dSAQuality} +# MAY CONTAIN { +# subtreeMinimumQuality, +# subtreeMaximumQuality} +# ::= {pilotObjectClass 22} +# +# +# +# +# -- Standard Attribute Types +# +# objectClass ObjectClass +# ::= {attributeType 0} +# +# +# aliasedObjectName AliasedObjectName +# ::= {attributeType 1} +# +# +# knowledgeInformation ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreString +# ::= {attributeType 2} +# +# +# commonName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-common-name)) +# ::= {attributeType 3} +# +# +# surname ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-surname)) +# ::= {attributeType 4} +# +# +# serialNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX printableStringSyntax +# (SIZE (1..ub-serial-number)) +# ::= {attributeType 5} +# +# +# countryName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX PrintableString +# (SIZE (1..ub-country-code)) +# SINGLE VALUE +# ::= {attributeType 6} +# +# +# localityName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-locality-name)) +# ::= {attributeType 7} +# +# +# stateOrProvinceName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-state-name)) +# ::= {attributeType 8} +# +# +# streetAddress ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-street-address)) +# ::= {attributeType 9} +# +# +# organizationName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-organization-name)) +# ::= {attributeType 10} +# +# +# organizationalUnitName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-organizational-unit-name)) +# ::= {attributeType 11} +# +# +# title ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-title)) +# ::= {attributeType 12} +# +# +# description ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-description)) +# ::= {attributeType 13} +# +# +# searchGuide ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX Guide +# ::= {attributeType 14} +# +# +# businessCategory ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-business-category)) +# ::= {attributeType 15} +# +# +# postalAddress ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX PostalAddress +# MATCHES FOR EQUALITY +# ::= {attributeType 16} +# +# +# postalCode ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-postal-code)) +# ::= {attributeType 17} +# +# +# postOfficeBox ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-post-office-box)) +# ::= {attributeType 18} +# +# +# physicalDeliveryOfficeName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX caseIgnoreStringSyntax +# (SIZE (1..ub-physical-office-name)) +# ::= {attributeType 19} +# +# +# telephoneNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX telephoneNumberSyntax +# (SIZE (1..ub-telephone-number)) +# ::= {attributeType 20} +# +# +# telexNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX TelexNumber +# (SIZE (1..ub-telex)) +# ::= {attributeType 21} +# +# +# teletexTerminalIdentifier ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX TeletexTerminalIdentifier +# (SIZE (1..ub-teletex-terminal-id)) +# ::= {attributeType 22} +# +# +# facsimileTelephoneNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX FacsimileTelephoneNumber +# ::= {attributeType 23} +# +# +# x121Address ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX NumericString +# (SIZE (1..ub-x121-address)) +# ::= {attributeType 24} +# +# +# internationaliSDNNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX NumericString +# (SIZE (1..ub-isdn-address)) +# ::= {attributeType 25} +# +# +# registeredAddress ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX PostalAddress +# ::= {attributeType 26} +# +# +# destinationIndicator ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX PrintableString +# (SIZE (1..ub-destination-indicator)) +# MATCHES FOR EQUALITY SUBSTRINGS +# ::= {attributeType 27} +# +# +# preferredDeliveryMethod ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX deliveryMethod +# ::= {attributeType 28} +# +# +# presentationAddress ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX PresentationAddress +# MATCHES FOR EQUALITY +# ::= {attributeType 29} +# +# +# supportedApplicationContext ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX objectIdentifierSyntax +# ::= {attributeType 30} +# +# +# member ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX distinguishedNameSyntax +# ::= {attributeType 31} +# +# +# owner ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX distinguishedNameSyntax +# ::= {attributeType 32} +# +# +# roleOccupant ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX distinguishedNameSyntax +# ::= {attributeType 33} +# +# +# seeAlso ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX distinguishedNameSyntax +# ::= {attributeType 34} +# +# +# userPassword ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX Userpassword +# ::= {attributeType 35} +# +# +# userCertificate ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX UserCertificate +# ::= {attributeType 36} +# +# +# cACertificate ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX cACertificate +# ::= {attributeType 37} +# +# +# authorityRevocationList ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX AuthorityRevocationList +# ::= {attributeType 38} +# +# +# certificateRevocationList ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX CertificateRevocationList +# ::= {attributeType 39} +# +# +# crossCertificatePair ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX CrossCertificatePair +# ::= {attributeType 40} +# +# +# +# +# -- Standard MHS Attribute Types +# +# mhsDeliverableContentLength ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX integer +# ::= {mhsAttributeType 0} +# +# +# mhsDeliverableContentTypes ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX oID +# ::= {mhsAttributeType 1} +# +# +# mhsDeliverableEits ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX oID +# ::= {mhsAttributeType 2} +# +# +# mhsDLMembers ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX oRName +# ::= {mhsAttributeType 3} +# +# +# mhsDLSubmitPermissions ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX dLSubmitPermission +# ::= {mhsAttributeType 4} +# +# +# mhsMessageStoreName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX dN +# ::= {mhsAttributeType 5} +# +# +# mhsORAddresses ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX oRAddress +# ::= {mhsAttributeType 6} +# +# +# mhsPreferredDeliveryMethods ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX deliveryMethod +# ::= {mhsAttributeType 7} +# +# +# mhsSupportedAutomaticActions ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX oID +# ::= {mhsAttributeType 8} +# +# +# mhsSupportedContentTypes ATTRIBUTE +# +# WITH ATTRIBUTE-SYNTAX oID +# ::= {mhsAttributeType 9} +# +# +# mhsSupportedOptionalAttributes ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX oID +# ::= {mhsAttributeType 10} +# +# +# +# +# -- Pilot Attribute Types +# +# userid ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-user-identifier)) +# ::= {pilotAttributeType 1} +# +# +# textEncodedORAddress ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-text-encoded-or-address)) +# ::= {pilotAttributeType 2} +# +# +# rfc822Mailbox ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreIA5StringSyntax +# (SIZE (1 .. ub-rfc822-mailbox)) +# ::= {pilotAttributeType 3} +# +# +# info ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-information)) +# ::= {pilotAttributeType 4} +# +# +# favouriteDrink ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-favourite-drink)) +# ::= {pilotAttributeType 5} +# +# +# roomNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-room-number)) +# ::= {pilotAttributeType 6} +# +# +# photo ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# CHOICE { +# g3-facsimile [3] G3FacsimileBodyPart +# } +# (SIZE (1 .. ub-photo)) +# ::= {pilotAttributeType 7} +# +# +# userClass ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-user-class)) +# ::= {pilotAttributeType 8} +# +# +# host ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-host)) +# ::= {pilotAttributeType 9} +# +# +# manager ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 10} +# +# +# documentIdentifier ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-document-identifier)) +# ::= {pilotAttributeType 11} +# +# +# documentTitle ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-document-title)) +# ::= {pilotAttributeType 12} +# +# +# documentVersion ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-document-version)) +# ::= {pilotAttributeType 13} +# +# +# documentAuthor ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 14} +# +# +# documentLocation ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-document-location)) +# ::= {pilotAttributeType 15} +# +# +# homeTelephoneNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# telephoneNumberSyntax +# ::= {pilotAttributeType 20} +# +# +# secretary ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 21} +# +# +# otherMailbox ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# SEQUENCE { +# mailboxType PrintableString, -- e.g. Telemail +# mailbox IA5String -- e.g. X378:Joe +# } +# ::= {pilotAttributeType 22} +# +# +# lastModifiedTime ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# uTCTimeSyntax +# ::= {pilotAttributeType 23} +# +# +# lastModifiedBy ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 24} +# +# +# domainComponent ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreIA5StringSyntax +# SINGLE VALUE +# ::= {pilotAttributeType 25} +# +# +# aRecord ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# DNSRecordSyntax +# ::= {pilotAttributeType 26} +# +# +# mXRecord ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# DNSRecordSyntax +# ::= {pilotAttributeType 28} +# +# +# nSRecord ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# DNSRecordSyntax +# ::= {pilotAttributeType 29} +# +# sOARecord ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# DNSRecordSyntax +# ::= {pilotAttributeType 30} +# +# +# cNAMERecord ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# iA5StringSyntax +# ::= {pilotAttributeType 31} +# +# +# associatedDomain ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreIA5StringSyntax +# ::= {pilotAttributeType 37} +# +# +# associatedName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 38} +# +# +# homePostalAddress ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# postalAddress +# MATCHES FOR EQUALITY +# ::= {pilotAttributeType 39} +# +# +# personalTitle ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-personal-title)) +# ::= {pilotAttributeType 40} +# +# +# mobileTelephoneNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# telephoneNumberSyntax +# ::= {pilotAttributeType 41} +# +# +# pagerTelephoneNumber ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# telephoneNumberSyntax +# ::= {pilotAttributeType 42} +# +# +# friendlyCountryName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# ::= {pilotAttributeType 43} +# +# +# uniqueIdentifier ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-unique-identifier)) +# ::= {pilotAttributeType 44} +# +# +# organizationalStatus ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-organizational-status)) +# ::= {pilotAttributeType 45} +# +# +# janetMailbox ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreIA5StringSyntax +# (SIZE (1 .. ub-janet-mailbox)) +# ::= {pilotAttributeType 46} +# +# +# mailPreferenceOption ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX ENUMERATED { +# no-list-inclusion(0), +# any-list-inclusion(1), -- may be added to any lists +# professional-list-inclusion(2) +# -- may be added to lists +# -- which the list provider +# -- views as related to the +# -- users professional inter- +# -- ests, perhaps evaluated +# -- from the business of the +# -- organisation or keywords +# -- in the entry. +# } +# ::= {pilotAttributeType 47} +# +# +# buildingName ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# caseIgnoreStringSyntax +# (SIZE (1 .. ub-building-name)) +# ::= {pilotAttributeType 48} +# +# +# dSAQuality ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX DSAQualitySyntax +# SINGLE VALUE +# ::= {pilotAttributeType 49} +# +# +# singleLevelQuality ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX DataQualitySyntax +# SINGLE VALUE +# +# +# subtreeMinimumQuality ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX DataQualitySyntax +# SINGLE VALUE +# -- Defaults to singleLevelQuality +# ::= {pilotAttributeType 51} +# +# +# subtreeMaximumQuality ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX DataQualitySyntax +# SINGLE VALUE +# -- Defaults to singleLevelQuality +# ::= {pilotAttributeType 52} +# +# +# personalSignature ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# CHOICE { +# g3-facsimile [3] G3FacsimileBodyPart +# } +# (SIZE (1 .. ub-personal-signature)) +# ::= {pilotAttributeType 53} +# +# +# dITRedirect ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# distinguishedNameSyntax +# ::= {pilotAttributeType 54} +# +# +# audio ATTRIBUTE +# WITH ATTRIBUTE-SYNTAX +# Audio +# (SIZE (1 .. ub-audio)) +# ::= {pilotAttributeType 55} +# +# documentPublisher ATTRIBUTE +# WITH ATTRIBUTE SYNTAX caseIgnoreStringSyntax +# ::= {pilotAttributeType 56} +# +# +# +# -- Generally useful syntaxes +# +# +# caseIgnoreIA5StringSyntax ATTRIBUTE-SYNTAX +# IA5String +# MATCHES FOR EQUALITY SUBSTRINGS +# +# +# iA5StringSyntax ATTRIBUTE-SYNTAX +# IA5String +# MATCHES FOR EQUALITY SUBSTRINGS +# +# +# -- Syntaxes to support the DNS attributes +# +# DNSRecordSyntax ATTRIBUTE-SYNTAX +# IA5String +# MATCHES FOR EQUALITY +# +# +# NRSInformationSyntax ATTRIBUTE-SYNTAX +# NRSInformation +# MATCHES FOR EQUALITY +# +# +# NRSInformation ::= SET { +# [0] Context, +# [1] Address-space-id, +# routes [2] SEQUENCE OF SEQUENCE { +# Route-cost, +# Addressing-info } +# } +# +# +# -- Upper bounds on length of attribute values +# +# +# ub-document-identifier INTEGER ::= 256 +# +# ub-document-location INTEGER ::= 256 +# +# ub-document-title INTEGER ::= 256 +# +# ub-document-version INTEGER ::= 256 +# +# ub-favourite-drink INTEGER ::= 256 +# +# ub-host INTEGER ::= 256 +# +# ub-information INTEGER ::= 2048 +# +# ub-unique-identifier INTEGER ::= 256 +# +# ub-personal-title INTEGER ::= 256 +# +# ub-photo INTEGER ::= 250000 +# +# ub-rfc822-mailbox INTEGER ::= 256 +# +# ub-room-number INTEGER ::= 256 +# +# ub-text-or-address INTEGER ::= 256 +# +# ub-user-class INTEGER ::= 256 +# +# ub-user-identifier INTEGER ::= 256 +# +# ub-organizational-status INTEGER ::= 256 +# +# ub-janet-mailbox INTEGER ::= 256 +# +# ub-building-name INTEGER ::= 256 +# +# ub-personal-signature ::= 50000 +# +# ub-audio INTEGER ::= 250000 +# +# [remainder of memo trimmed] + diff --git a/test/ldap_files/schema/inetorgperson.schema b/test/ldap_files/schema/inetorgperson.schema new file mode 100644 index 00000000..db0b8c11 --- /dev/null +++ b/test/ldap_files/schema/inetorgperson.schema @@ -0,0 +1,155 @@ +# inetorgperson.schema -- InetOrgPerson (RFC2798) +# $OpenLDAP$ +## This work is part of OpenLDAP Software . +## +## 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 +## . +# +# 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 ) + ) diff --git a/test/ldap_files/schema/mailserver.schema b/test/ldap_files/schema/mailserver.schema new file mode 100644 index 00000000..ff502ff1 --- /dev/null +++ b/test/ldap_files/schema/mailserver.schema @@ -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 ) + ) diff --git a/test/ldap_files/schema/nis.schema b/test/ldap_files/schema/nis.schema new file mode 100644 index 00000000..d970998e --- /dev/null +++ b/test/ldap_files/schema/nis.schema @@ -0,0 +1,237 @@ +# $OpenLDAP$ +## This work is part of OpenLDAP Software . +## +## 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 +## . + +# 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 ) ) diff --git a/test/ldap_files/schema/sudo.schema b/test/ldap_files/schema/sudo.schema new file mode 100644 index 00000000..d3e95e00 --- /dev/null +++ b/test/ldap_files/schema/sudo.schema @@ -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 ) + ) diff --git a/test/ldap_files/schema/yunohost.schema b/test/ldap_files/schema/yunohost.schema new file mode 100644 index 00000000..7da60a20 --- /dev/null +++ b/test/ldap_files/schema/yunohost.schema @@ -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 ) ) diff --git a/test/ldap_files/slapd.conf.template b/test/ldap_files/slapd.conf.template new file mode 100644 index 00000000..05c3f522 --- /dev/null +++ b/test/ldap_files/slapd.conf.template @@ -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 \ No newline at end of file diff --git a/test/ldap_files/tests.ldif b/test/ldap_files/tests.ldif new file mode 100644 index 00000000..355dd643 --- /dev/null +++ b/test/ldap_files/tests.ldif @@ -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 diff --git a/test/locales/en.json b/test/locales/en.json index e63d37b6..838149df 100644 --- a/test/locales/en.json +++ b/test/locales/en.json @@ -1,3 +1,5 @@ { - "foo": "bar" + "foo": "bar", + "Dummy Password": "Dummy Password", + "Dummy Yoloswag Password": "Dummy Yoloswag Password" } diff --git a/test/src/ldap_server.py b/test/src/ldap_server.py new file mode 100644 index 00000000..710f04a6 --- /dev/null +++ b/test/src/ldap_server.py @@ -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) diff --git a/test/src/old_slapdtest/README b/test/src/old_slapdtest/README new file mode 100644 index 00000000..a6e34008 --- /dev/null +++ b/test/src/old_slapdtest/README @@ -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 \ No newline at end of file diff --git a/test/src/old_slapdtest/__init__.py b/test/src/old_slapdtest/__init__.py new file mode 100644 index 00000000..a80062b1 --- /dev/null +++ b/test/src/old_slapdtest/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from ._slapdtest import SlapdObject diff --git a/test/src/old_slapdtest/_slapdtest.py b/test/src/old_slapdtest/_slapdtest.py new file mode 100644 index 00000000..ab40ff55 --- /dev/null +++ b/test/src/old_slapdtest/_slapdtest.py @@ -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() diff --git a/test/src/old_slapdtest/certs/README b/test/src/old_slapdtest/certs/README new file mode 100644 index 00000000..4be616ae --- /dev/null +++ b/test/src/old_slapdtest/certs/README @@ -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. diff --git a/test/src/old_slapdtest/certs/ca.conf b/test/src/old_slapdtest/certs/ca.conf new file mode 100644 index 00000000..d1d89e18 --- /dev/null +++ b/test/src/old_slapdtest/certs/ca.conf @@ -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 diff --git a/test/src/old_slapdtest/certs/ca.pem b/test/src/old_slapdtest/certs/ca.pem new file mode 100644 index 00000000..b52ffafb --- /dev/null +++ b/test/src/old_slapdtest/certs/ca.pem @@ -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----- diff --git a/test/src/old_slapdtest/certs/client.conf b/test/src/old_slapdtest/certs/client.conf new file mode 100644 index 00000000..774dc3a4 --- /dev/null +++ b/test/src/old_slapdtest/certs/client.conf @@ -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" diff --git a/test/src/old_slapdtest/certs/client.key b/test/src/old_slapdtest/certs/client.key new file mode 100644 index 00000000..7213c0b4 --- /dev/null +++ b/test/src/old_slapdtest/certs/client.key @@ -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----- diff --git a/test/src/old_slapdtest/certs/client.pem b/test/src/old_slapdtest/certs/client.pem new file mode 100644 index 00000000..ca2989ca --- /dev/null +++ b/test/src/old_slapdtest/certs/client.pem @@ -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----- diff --git a/test/src/old_slapdtest/certs/gencerts.sh b/test/src/old_slapdtest/certs/gencerts.sh new file mode 100755 index 00000000..8a99db58 --- /dev/null +++ b/test/src/old_slapdtest/certs/gencerts.sh @@ -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 diff --git a/test/src/old_slapdtest/certs/gennssdb.sh b/test/src/old_slapdtest/certs/gennssdb.sh new file mode 100755 index 00000000..aeeb3331 --- /dev/null +++ b/test/src/old_slapdtest/certs/gennssdb.sh @@ -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 \ No newline at end of file diff --git a/test/src/old_slapdtest/certs/server.conf b/test/src/old_slapdtest/certs/server.conf new file mode 100644 index 00000000..94f4427a --- /dev/null +++ b/test/src/old_slapdtest/certs/server.conf @@ -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" diff --git a/test/src/old_slapdtest/certs/server.key b/test/src/old_slapdtest/certs/server.key new file mode 100644 index 00000000..a8916701 --- /dev/null +++ b/test/src/old_slapdtest/certs/server.key @@ -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----- diff --git a/test/src/old_slapdtest/certs/server.pem b/test/src/old_slapdtest/certs/server.pem new file mode 100644 index 00000000..25ba06c0 --- /dev/null +++ b/test/src/old_slapdtest/certs/server.pem @@ -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----- diff --git a/test/src/testauth.py b/test/src/testauth.py index 0a28f9c4..853cb15b 100644 --- a/test/src/testauth.py +++ b/test/src/testauth.py @@ -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" diff --git a/test/test_actionsmap.py b/test/test_actionsmap.py index 11e97d9d..b69b5179 100644 --- a/test/test_actionsmap.py +++ b/test/test_actionsmap.py @@ -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 + ) diff --git a/test/test_auth.py b/test/test_auth.py index 392c9af3..dd95d9c7 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -1,91 +1,343 @@ import os +import pytest + +from moulinette import MoulinetteError +from moulinette import m18n -def login(webapi, csrf=False, profile=None, status=200): +class TestAuthAPI: + def login(self, webapi, csrf=False, profile=None, status=200, password="default"): + data = {"password": password} + if profile: + data["profile"] = profile - data = {"password": "Yoloswag"} - if profile: - data["profile"] = profile + return webapi.post( + "/login", + data, + status=status, + headers=None if csrf else {"X-Requested-With": ""}, + ) - return webapi.post( - "/login", - data, - status=status, - headers=None if csrf else {"X-Requested-With": ""}, - ) + 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(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_request_not_logged_only_api(self, moulinette_webapi): + assert ( + moulinette_webapi.get("/test-auth/only-api", status=401).text + == "Authentication required" + ) + + 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 + + cache_session_default = os.environ["MOULINETTE_CACHE_DIR"] + "/session/default/" + assert moulinette_webapi.cookies["session.id"] + ".asc" in os.listdir( + cache_session_default + ) + + def test_login_bad_password(self, moulinette_webapi): + assert ( + self.login(moulinette_webapi, password="Bad Password", status=401).text + == "Invalid password" + ) + + 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 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(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(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(self, moulinette_webapi): + self.login(moulinette_webapi) + + moulinette_webapi.get("/logout", status=200) + + cache_session_default = os.environ["MOULINETTE_CACHE_DIR"] + "/session/default/" + assert not moulinette_webapi.cookies["session.id"] + ".asc" in os.listdir( + cache_session_default + ) + + assert ( + 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) -def test_request_no_auth_needed(moulinette_webapi): +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 ( - moulinette_webapi.get("/test-auth/none", status=200).text - == '"some_data_from_none"' - ) + assert "some_data_from_default" in message.out + moulinette_cli.run( + ["testauth", "default"], output_as="plain", password="default" + ) + message = capsys.readouterr() -def test_request_with_auth_but_not_logged(moulinette_webapi): + assert "some_data_from_default" in message.out - assert ( - moulinette_webapi.get("/test-auth/default", status=401).text - == "Authentication required" - ) + 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(moulinette_webapi): + 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") - assert login(moulinette_webapi).text == "Logged in" + translation = m18n.g("invalid_password") + expected_msg = translation.format() + assert expected_msg in str(exception) - assert "session.id" in moulinette_webapi.cookies - assert "session.tokens" in moulinette_webapi.cookies + with pytest.raises(MoulinetteError) as exception: + moulinette_cli.run( + ["testauth", "default"], output_as="none", password="yoloswag" + ) - cache_session_default = os.environ["MOULINETTE_CACHE_DIR"] + "/session/default/" - assert moulinette_webapi.cookies["session.id"] + ".asc" in os.listdir( - cache_session_default - ) + 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() -def test_login_csrf_attempt(moulinette_webapi): + assert "some_data_from_none" in message.out - # C.f. - # https://security.stackexchange.com/a/58308 - # https://stackoverflow.com/a/22533680 + def test_request_not_logged_only_api(self, capsys, moulinette_cli): + moulinette_cli.run(["testauth", "only-api"], output_as="plain") + message = capsys.readouterr() - assert "CSRF protection" in 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) + 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") -def test_login_then_legit_request_without_cookies(moulinette_webapi): + message = capsys.readouterr() - login(moulinette_webapi) + assert "some_data_from_only_cli" in message.out - moulinette_webapi.cookiejar.clear() + 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") - moulinette_webapi.get("/test-auth/default", status=401) + 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_login_then_legit_request(moulinette_webapi): + 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() - login(moulinette_webapi) + assert "666" in message.out - assert ( - moulinette_webapi.get("/test-auth/default", status=200).text - == '"some_data_from_default"' - ) + moulinette_cli.run(["-v"], output_as="plain") + message = capsys.readouterr() + assert "666" in message.out -def test_login_then_logout(moulinette_webapi): + with pytest.raises(MoulinetteError): + moulinette_cli.run(["--wersion"], output_as="plain") + message = capsys.readouterr() - login(moulinette_webapi) + assert "cannot get value from callback method" in message.err - moulinette_webapi.get("/logout", status=200) + 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() - cache_session_default = os.environ["MOULINETTE_CACHE_DIR"] + "/session/default/" - assert not moulinette_webapi.cookies["session.id"] + ".asc" in os.listdir( - cache_session_default - ) + assert "yoloswag" in message.out - assert ( - moulinette_webapi.get("/test-auth/default", status=401).text - == "Authentication required" - ) + 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 diff --git a/test/test_filesystem.py b/test/test_filesystem.py index 14a80ecb..87dfbaa0 100644 --- a/test/test_filesystem.py +++ b/test/test_filesystem.py @@ -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)) diff --git a/test/test_ldap.py b/test/test_ldap.py new file mode 100644 index 00000000..fd1fcb4e --- /dev/null +++ b/test/test_ldap.py @@ -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"}) diff --git a/test/test_network.py b/test/test_network.py index 1887d6f1..6d5c574d 100644 --- a/test/test_network.py +++ b/test/test_network.py @@ -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) diff --git a/test/test_process.py b/test/test_process.py index 2565a732..c18cdbe7 100644 --- a/test/test_process.py +++ b/test/test_process.py @@ -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" diff --git a/test/test_text.py b/test/test_text.py index 58627597..1bf1f896 100644 --- a/test/test_text.py +++ b/test/test_text.py @@ -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")