From 27ca4dfce37f7830e83eaec87c36d5f7261dc3c6 Mon Sep 17 00:00:00 2001 From: Fanch Date: Thu, 24 Jul 2025 21:47:54 +0200 Subject: [PATCH] init python --- bin/createUser.py | 5 ++ bin/lib/config.py | 14 ++++ bin/lib/ldap.py | 99 ++++++++++++++++++++++ bin/lib/paheko.py | 140 +++++++++++++++++++++++++++++++ bin/lib/user.py | 209 ++++++++++++++++++++++++++++++++++++++++++++++ bin/lib/utils.py | 5 ++ 6 files changed, 472 insertions(+) create mode 100644 bin/createUser.py create mode 100644 bin/lib/config.py create mode 100644 bin/lib/ldap.py create mode 100644 bin/lib/paheko.py create mode 100644 bin/lib/user.py create mode 100644 bin/lib/utils.py diff --git a/bin/createUser.py b/bin/createUser.py new file mode 100644 index 0000000..1847fe8 --- /dev/null +++ b/bin/createUser.py @@ -0,0 +1,5 @@ +#!/usr/bin/python3 + +from lib.user import create_users_from_file + +create_users_from_file() \ No newline at end of file diff --git a/bin/lib/config.py b/bin/lib/config.py new file mode 100644 index 0000000..603e78b --- /dev/null +++ b/bin/lib/config.py @@ -0,0 +1,14 @@ +DOCKERS_ENV = "/kaz/config/dockers.env" +SECRETS = "/kaz/secret/env-{serv}" + +def getDockersConfig(key): + with open(DOCKERS_ENV) as config: + for line in config: + if line.startswith(f"{key}="): + return line.split("=", 1)[1].split("#")[0].strip() + +def getSecretConfig(serv, key): + with open(SECRETS.format(serv=serv)) as config: + for line in config: + if line.startswith(f"{key}="): + return line.split("=", 2)[1].split("#")[0].strip() diff --git a/bin/lib/ldap.py b/bin/lib/ldap.py new file mode 100644 index 0000000..d849495 --- /dev/null +++ b/bin/lib/ldap.py @@ -0,0 +1,99 @@ +import ldap +from passlib.hash import sha512_crypt +from email_validator import validate_email, EmailNotValidError + +from .config import getDockersConfig, getSecretConfig + +class Ldap: + + def __init__(self): + self.ldap_connection = None + self.ldap_root = getDockersConfig("ldap_root") + self.ldap_admin_username = getSecretConfig("ldapServ", "LDAP_ADMIN_USERNAME") + self.ldap_admin_password = getSecretConfig("ldapServ", "LDAP_ADMIN_PASSWORD") + self.ldap_host = "10.0.0.146" + + def __enter__(self): + self.ldap_connection = ldap.initialize(f"ldap://{self.ldap_host}") + self.ldap_connection.simple_bind_s("cn={},{}".format(self.ldap_admin_username, self.ldap_root), self.ldap_admin_password) + return self + + def __exit__(self, tp, e, traceback): + self.ldap_connection.unbind_s() + + + def get_email(self, email): + """ + Vérifier si un utilisateur avec cet email existe dans le LDAP soit comme mail principal soit comme alias + """ + # Créer une chaîne de filtre pour rechercher dans les champs "cn" et "mailAlias" + filter_str = "(|(cn={})(mailAlias={}))".format(email, email) + result = self.ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, filter_str) + return result + + + def delete_user(self, email): + """ + Supprimer un utilisateur du LDAP par son adresse e-mail + """ + try: + # Recherche de l'utilisateur + result = self.ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, "(cn={})".format(email)) + + if not result: + return False # Utilisateur non trouvé + + # Récupération du DN de l'utilisateur + dn = result[0][0] + + # Suppression de l'utilisateur + self.ldap_connection.delete_s(dn) + return True # Utilisateur supprimé avec succès + + except ldap.NO_SUCH_OBJECT: + return False # Utilisateur non trouvé + except ldap.LDAPError as e: + return False # Erreur lors de la suppression + + + + def create_user(self, email, prenom, nom, password, email_secours, quota): + """ + Créer une nouvelle entrée dans le LDAP pour un nouvel utilisateur. QUESTION: A QUOI SERVENT PRENOM/NOM/IDENT_KAZ DANS LE LDAP ? POURQUOI 3 QUOTA ? + """ + password_chiffre = sha512_crypt.hash(password) + + if not validate_email(email) or not validate_email(email_secours): + return False + + if self.get_email(email): + return False + + # Construire le DN + dn = f"cn={email},ou=users,{self.ldap_root}" + + mod_attrs = [ + ('objectClass', [b'inetOrgPerson', b'PostfixBookMailAccount', b'nextcloudAccount', b'kaznaute']), + ('sn', f'{prenom} {nom}'.encode('utf-8')), + ('mail', email.encode('utf-8')), + ('mailEnabled', b'TRUE'), + ('mailGidNumber', b'5000'), + ('mailHomeDirectory', f"/var/mail/{email.split('@')[1]}/{email.split('@')[0]}/".encode('utf-8')), + ('mailQuota', f'{quota}G'.encode('utf-8')), + ('mailStorageDirectory', f"maildir:/var/mail/{email.split('@')[1]}/{email.split('@')[0]}/".encode('utf-8')), + ('mailUidNumber', b'5000'), + ('mailDeSecours', email_secours.encode('utf-8')), + ('identifiantKaz', f'{prenom.lower()}.{nom.lower()}'.encode('utf-8')), + ('quota', str(quota).encode('utf-8')), + ('nextcloudEnabled', b'TRUE'), + ('nextcloudQuota', f'{quota} GB'.encode('utf-8')), + ('mobilizonEnabled', b'TRUE'), + ('agoraEnabled', b'TRUE'), + ('userPassword', f'{{CRYPT}}{password_chiffre}'.encode('utf-8')), + ('cn', email.encode('utf-8')) + ] + + self.ldap_connection.add_s(dn, mod_attrs) + return True + + diff --git a/bin/lib/paheko.py b/bin/lib/paheko.py new file mode 100644 index 0000000..ef173e4 --- /dev/null +++ b/bin/lib/paheko.py @@ -0,0 +1,140 @@ +import requests + +from .config import getDockersConfig, getSecretConfig + +paheko_ident = getDockersConfig("paheko_API_USER") +paheko_pass = getDockersConfig("paheko_API_PASSWORD") +paheko_url = f"https://kaz-paheko.{getDockersConfig('domain')}" + +class Paheko: + def get_categories(self): + """ + Récupérer les catégories Paheko avec le compteur associé + """ + global paheko_ident, paheko_pass, paheko_url + + auth = (paheko_ident, paheko_pass) + api_url = paheko_url + '/api/user/categories' + + response = requests.get(api_url, auth=auth) + + if response.status_code == 200: + data = response.json() + return data + else: + return None + + + def get_users_in_categorie(self,categorie): + """ + Afficher les membres d'une catégorie Paheko + """ + + global paheko_ident, paheko_pass, paheko_url + + auth = (paheko_ident, paheko_pass) + if not categorie.isdigit(): + return 'Id de category non valide', 400 + + api_url = paheko_url + '/api/user/category/'+categorie+'.json' + + response = requests.get(api_url, auth=auth) + + if response.status_code == 200: + data = response.json() + return data + else: + return None + + + def get_user(self,ident): + """ + Afficher un membre de Paheko par son email kaz ou son numéro ou le non court de l'orga + """ + + emailmatchregexp = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") + + if emailmatchregexp.match(ident): + data = { "sql": f"select * from users where email='{ident}' or alias = '{ident}'" } + api_url = self.paheko_url + '/api/sql/' + response = requests.post(api_url, auth=self.auth, data=data) + #TODO: if faut Rechercher count et vérifier que = 1 et supprimer le count=1 dans la réponse + elif ident.isdigit(): + api_url = self.paheko_url + '/api/user/'+ident + response = requests.get(api_url, auth=self.auth) + else: + nomorga = re.sub(r'\W+', '', ident) # on vire les caractères non alphanumérique + data = { "sql": f"select * from users where admin_orga=1 and nom_orga='{nomorga}'" } + api_url = self.paheko_url + '/api/sql/' + response = requests.post(api_url, auth=self.auth, data=data) + #TODO:if faut Rechercher count et vérifier que = 1 et supprimer le count=1 dans la réponse + + if response.status_code == 200: + data = response.json() + if data["count"] == 1: + return data["results"][0] + elif data["count"] == 0: + return None + else: + return data["results"] + else: + return None + + + def set_user(self,ident,field,new_value): + """ + Modifie la valeur d'un champ d'un membre paheko (ident= numéro paheko ou email kaz) + """ + + #récupérer le numero paheko si on fournit un email kaz + emailmatchregexp = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") + if emailmatchregexp.match(ident): + data = { "sql": f"select id from users where email='{ident}'" } + api_url = self.paheko_url + '/api/sql/' + response = requests.post(api_url, auth=self.auth, data=data) + if response.status_code == 200: + #on extrait l'id de la réponse + data = response.json() + if data['count'] == 0: + print("email non trouvé") + return None + elif data['count'] > 1: + print("trop de résultat") + return None + else: + #OK + ident = data['results'][0]['id'] + else: + print("pas de résultat") + return None + elif not ident.isdigit(): + print("Identifiant utilisateur invalide") + return None + + regexp = re.compile("[^a-zA-Z0-9 \\r\\n\\t" + re.escape(string.punctuation) + "]") + valeur = regexp.sub('',new_value) # mouais, il faudrait être beaucoup plus précis ici en fonction des champs qu'on accepte... + + champ = re.sub(r'\W+','',field) # pas de caractères non alphanumériques ici, dans l'idéal, c'est à choisir dans une liste plutot + + api_url = self.paheko_url + '/api/user/'+str(ident) + payload = {champ: valeur} + response = requests.post(api_url, auth=self.auth, data=payload) + return response.json() + + + def get_users_with_action(self, action): + """ + retourne tous les membres de paheko avec une action à mener (création du compte kaz / modification...) + """ + auth = (paheko_ident, paheko_pass) + + api_url = paheko_url + '/api/sql/' + payload = { "sql": f"select * from users where action_auto='{action}'" } + response = requests.post(api_url, auth=auth, data=payload) + + if response.status_code == 200: + return response.json() + else: + return None + + diff --git a/bin/lib/user.py b/bin/lib/user.py new file mode 100644 index 0000000..bbbe76c --- /dev/null +++ b/bin/lib/user.py @@ -0,0 +1,209 @@ +from email_validator import validate_email, EmailNotValidError +from glob import glob +import tempfile +import re + + +from .paheko import Paheko +from .ldap import Ldap +from .utils import generate_password + +DEFAULT_FILE = "/kaz/tmp/createUser.txt" + +#class Kaz_user: + + # def __init__(self): + # self.paheko = Paheko() + # self.ldap = Ldap() + # global sympa_liste_info + # + # self.paheko_users_action_resource = Paheko_users_action() + # self.paheko_user_resource=Paheko_user() + # self.ldap_user_resource = Ldap_user() + # self.password_create_resource = Password_create() + # self.mattermost_message_resource=Mattermost_message() + # self.mattermost_user_resource=Mattermost_user() + # self.mattermost_user_team_resource=Mattermost_user_team() + # self.mattermost_user_channel_resource=Mattermost_user_channel() + # self.mattermost_team_resource=Mattermost_team() + # self.sympa_user_resource=Sympa_user() + +def create_user(email, email_secours, admin_orga, nom_orga, quota_disque, nom, prenom, nc_orga, garradin_orga, wp_orga, agora_orga, wiki_orga, nc_base, groupe_nc_base, equipe_agora, password=None): + with Ldap() as ldap: + email = email.lower() + + # est-il déjà dans le ldap ? (mail ou alias) + if ldap.get_email(email): + print(f"ERREUR 1: {email} déjà existant dans ldap. on arrête tout") + return None + + #test nom orga + if admin_orga == 1: + if nom_orga is None: + print(f"ERREUR 0 sur paheko: {email} : nom_orga vide, on arrête tout") + return + if not bool(re.match(r'^[a-z0-9-]+$', nom_orga)): + print(f"ERREUR 0 sur paheko: {email} : nom_orga ({tab['nom_orga']}) incohérent (minuscule/chiffre/-), on arrête tout") + return + + #test email_secours + email_secours = email_secours.lower() + if not validate_email(email_secours): + print("Mauvais email de secours") + return + + #test quota + quota = quota_disque + if not quota.isdigit(): + print(f"ERREUR 2: quota non numérique : {quota}, on arrête tout") + return + + #on génère un password + password = password or generate_password() + + #on créé dans le ldap + #à quoi servent prenom/nom dans le ldap ? + data = { + "prenom": prenom, + "nom": nom, + "password": password, + "email_secours": email_secours, + "quota": quota + } + if not ldap.create_user(email, **data): + print("Erreur LDAP") + return + + # #on créé dans MM + # user = email.split('@')[0] + # res, status_code = self.mattermost_user_resource.post(user,email,password) + # if status_code != 200: raise ValueError(f"ERREUR 4 sur MM: {email} : {res}, on arrête tout ") + # + # # et on affecte à l'équipe kaz + # res, status_code = self.mattermost_user_team_resource.post(email,"kaz") + # if status_code != 200: raise ValueError(f"ERREUR 5 sur MM: {email} : {res}, on arrête tout ") + # + # #et aux 2 canaux de base + # res, status_code = self.mattermost_user_channel_resource.post(email,"kaz","une-question--un-soucis") + # if status_code != 200: raise ValueError(f"ERREUR 6 sur MM: {email} : {res}, on arrête tout ") + # res, status_code = self.mattermost_user_channel_resource.post(email,"kaz","cafe-du-commerce--ouvert-2424h") + # if status_code != 200: raise ValueError(f"ERREUR 7 sur MM: {email} : {res}, on arrête tout ") + # + # #on créé une nouvelle équipe ds MM si besoin + # if tab['admin_orga'] == 1: + # res, status_code = self.mattermost_team_resource.post(tab['nom_orga'],email) + # if status_code != 200: raise ValueError(f"ERREUR 8 sur MM: {email} : {res}, on arrête tout ") + # #BUG: créer la nouvelle équipe n'a pas rendu l'email admin, on le rajoute comme membre simple + # res, status_code = self.mattermost_user_team_resource.post(email,tab['nom_orga']) + # if status_code != 200: raise ValueError(f"ERREUR 8.1 sur MM: {email} : {res}, on arrête tout ") + # + # #on créé dans le cloud genéral + # #inutile car tous les user du ldap sont user du cloud général. + # + # #on inscrit email et email_secours à la nl sympa_liste_info + # res, status_code = self.sympa_user_resource.post(email,sympa_liste_info) + # if status_code != 200: raise ValueError(f"ERREUR 9 sur Sympa: {email} : {res}, on arrête tout ") + # res, status_code = self.sympa_user_resource.post(email_secours,sympa_liste_info) + # if status_code != 200: raise ValueError(f"ERREUR 10 sur Sympa: {email_secours} : {res}, on arrête tout ") + # + # #on construit/envoie le mail + # context = { + # 'ADMIN_ORGA': tab['admin_orga'], + # 'NOM': tab['nom'], + # 'EMAIL_SOUHAITE': email, + # 'PASSWORD': password, + # 'QUOTA': tab['quota_disque'], + # 'URL_WEBMAIL': webmail_url, + # 'URL_AGORA': mattermost_url, + # 'URL_MDP': mdp_url, + # 'URL_LISTE': sympa_url, + # 'URL_SITE': site_url, + # 'URL_CLOUD': cloud_url + # } + # subject="KAZ: confirmation d'inscription !" + # sender=app.config['MAIL_USERNAME'] + # reply_to = app.config['MAIL_REPLY_TO'] + # msg = Message(subject=subject, sender=sender, reply_to=reply_to, recipients=[email,email_secours]) + # msg.html = render_template('email_inscription.html', **context) + # mail.send(msg) + # + # #on met le flag paheko action à Aucune + # res, status_code = self.paheko_user_resource.put(email,"action_auto","Aucune") + # if status_code != 200: raise ValueError(f"ERREUR 12 sur paheko: {email} : {res}, on arrête tout ") + # + # #on post sur MM pour dire ok + # msg=f"**POST AUTO** Inscription réussie pour {email} avec le secours {email_secours} Bisou!" + # self.mattermost_message_resource.post(message=msg) + + +def create_waiting_users(): + """ + Créé les kaznautes en attente: inscription sur MM / Cloud / email + msg sur MM + email à partir de action="a créer" sur paheko + """ + #verrou pour empêcher de lancer en même temps la même api + prefixe="create_user_lock_" + if glob(f"{tempfile.gettempdir()}/{prefixe}*"): + print("Lock présent") + return None + lock_file = tempfile.NamedTemporaryFile(prefix=prefixe,delete=True) + + #qui sont les kaznautes à créer ? + paheko = Paheko() + liste_kaznautes = paheko.get_users_with_action("A créer") + + if liste_kaznautes: + count=liste_kaznautes['count'] + if count==0: + print("aucun nouveau kaznaute à créer") + return + + #au moins un kaznaute à créer + for tab in liste_kaznautes['results']: + create_user(**tab) + + print("fin des inscriptions") + + +def create_users_from_file(file=DEFAULT_FILE): + """ + Créé les kaznautes en attente: inscription sur MM / Cloud / email + msg sur MM + email à partir du ficher + """ + #verrou pour empêcher de lancer en même temps la même api + prefixe="create_user_lock_" + if glob(f"{tempfile.gettempdir()}/{prefixe}*"): + print("Lock présent") + return None + lock_file = tempfile.NamedTemporaryFile(prefix=prefixe,delete=True) + + #qui sont les kaznautes à créer ? + liste_kaznautes = [] + with open(file) as lines: + for line in lines: + line = line.strip() + if not line.startswith("#") and line != "": + user_data = line.split(';') + user_dict = { + "nom": user_data[0], + "prenom": user_data[1], + "email": user_data[2], + "email_secours": user_data[3], + "nom_orga": user_data[4], + "admin_orga": user_data[5], + "nc_orga": user_data[6], + "garradin_orga": user_data[7], + "wp_orga": user_data[8], + "agora_orga": user_data[9], + "wiki_orga": user_data[10], + "nc_base": user_data[11], + "groupe_nc_base": user_data[12], + "equipe_agora": user_data[13], + "quota_disque": user_data[14], + "password": user_data[15], + } + liste_kaznautes.append(user_dict) + + if liste_kaznautes: + for tab in liste_kaznautes: + create_user(**tab) + + print("fin des inscriptions") \ No newline at end of file diff --git a/bin/lib/utils.py b/bin/lib/utils.py new file mode 100644 index 0000000..80260ee --- /dev/null +++ b/bin/lib/utils.py @@ -0,0 +1,5 @@ +def generate_password(self): + cmd="apg -n 1 -m 10 -M NCL -d" + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + new_password="_"+output.decode("utf-8")+"_" + return new_password