From f796f01d4096088ada1eb1cc45bea89dd7005b75 Mon Sep 17 00:00:00 2001 From: hpl Date: Tue, 20 Aug 2024 01:53:17 +0200 Subject: [PATCH] =?UTF-8?q?=20Modifications=20qui=20seront=20valid=C3=A9es?= =?UTF-8?q?=20:=20=09modifi=C3=A9=C2=A0:=20=20=20=20=20=20=20=20=20../app.?= =?UTF-8?q?py=20=09nouveau=20fichier=C2=A0:=20cloud.py=20=09nouveau=20fich?= =?UTF-8?q?ier=C2=A0:=20common=5Fimports.py=20=09nouveau=20fichier=C2=A0:?= =?UTF-8?q?=20config.py=20=09nouveau=20fichier=C2=A0:=20dns.py=20=09nouvea?= =?UTF-8?q?u=20fichier=C2=A0:=20kaz=5Fuser.py=20=09nouveau=20fichier=C2=A0?= =?UTF-8?q?:=20ldap.py=20=09nouveau=20fichier=C2=A0:=20mattermost.py=20=09?= =?UTF-8?q?nouveau=20fichier=C2=A0:=20paheko.py=20=09nouveau=20fichier?= =?UTF-8?q?=C2=A0:=20password.py=20=09nouveau=20fichier=C2=A0:=20quota.py?= =?UTF-8?q?=20=09nouveau=20fichier=C2=A0:=20sympa.py=20=09nouveau=20fichie?= =?UTF-8?q?r=C2=A0:=20test.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dockers/apikaz/source/app.py | 1837 +---------------- dockers/apikaz/source/resources/cloud.py | 147 ++ .../apikaz/source/resources/common_imports.py | 25 + dockers/apikaz/source/resources/config.py | 45 + dockers/apikaz/source/resources/dns.py | 159 ++ dockers/apikaz/source/resources/kaz_user.py | 223 ++ dockers/apikaz/source/resources/ldap.py | 341 +++ dockers/apikaz/source/resources/mattermost.py | 394 ++++ dockers/apikaz/source/resources/paheko.py | 256 +++ dockers/apikaz/source/resources/password.py | 34 + dockers/apikaz/source/resources/quota.py | 64 + dockers/apikaz/source/resources/sympa.py | 81 + dockers/apikaz/source/resources/test.py | 111 + 13 files changed, 1903 insertions(+), 1814 deletions(-) create mode 100644 dockers/apikaz/source/resources/cloud.py create mode 100644 dockers/apikaz/source/resources/common_imports.py create mode 100644 dockers/apikaz/source/resources/config.py create mode 100644 dockers/apikaz/source/resources/dns.py create mode 100644 dockers/apikaz/source/resources/kaz_user.py create mode 100644 dockers/apikaz/source/resources/ldap.py create mode 100644 dockers/apikaz/source/resources/mattermost.py create mode 100644 dockers/apikaz/source/resources/paheko.py create mode 100644 dockers/apikaz/source/resources/password.py create mode 100644 dockers/apikaz/source/resources/quota.py create mode 100644 dockers/apikaz/source/resources/sympa.py create mode 100644 dockers/apikaz/source/resources/test.py diff --git a/dockers/apikaz/source/app.py b/dockers/apikaz/source/app.py index 50d1afe..b5a36e9 100644 --- a/dockers/apikaz/source/app.py +++ b/dockers/apikaz/source/app.py @@ -1,35 +1,31 @@ -import os -import re -import requests -import subprocess -import logging -import tempfile -import ldap -import imaplib -import random -import string -import json +# Importer tous les modules depuis common_imports (c'est là où il faut rajouter des modules python manquant) +from resources.common_imports import * -from flask import Flask, jsonify, send_from_directory, request, abort, json, Response, render_template -from flask_mail import Mail, Message -from flasgger import Swagger -from flask_restful import Api, Resource -from flask_jwt_extended import JWTManager, create_access_token, jwt_required +# Importer toutes les classes de kaz +from resources.password import Password_create +from resources.paheko import Paheko_categories, Paheko_users, Paheko_user, Paheko_users_action +from resources.mattermost import Mattermost_authenticate, Mattermost_user, Mattermost_message, Mattermost_user_team, Mattermost_user_channel, Mattermost_team +from resources.ldap import Ldap_user +from resources.cloud import Cloud_user, Cloud_user_delete +from resources.sympa import Sympa_user +from resources.quota import Quota +from resources.dns import Dns_serveurs, Dns +from resources.kaz_user import Kaz_user +from resources.test import Test -from passlib.hash import sha512_crypt -from unidecode import unidecode -from email_validator import validate_email, EmailNotValidError -from time import sleep -from glob import glob -from bs4 import BeautifulSoup -from datetime import datetime + +#on importe toutes les variables globales +from resources.config import * app = Flask(__name__) jwt = JWTManager(app) api = Api(app) +#comment qu'on log ? (TODO:faudrait coller ça dans le docker-compose.yml) app.logger.setLevel(logging.DEBUG) + +#on décrit l'api telle qu'elle apparait dans le swagger (la doc) swagger = Swagger(app, template={ "swagger": "2.0", "info": { @@ -71,12 +67,7 @@ swagger = Swagger(app, template={ # fail2ban (ou alors sur traefik) # découper app.py en service # quels scripts bash garder ? - -#************************************************* - -# Configuration du logger -# logging.basicConfig(level=logging.INFO) -# logger = logging.getLogger(__name__) # Création de l'objet logger +#fin TODO #************************************************* #Filtrer les IP qui peuvent accéder à l'api @@ -106,35 +97,6 @@ trusted_ips = [ #app.config['JWT_SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY') app.config['JWT_SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY', 'your_jwt_secret_key') -#le paheko de kaz -paheko_ident=os.environ.get('paheko_API_USER') -paheko_pass=os.environ.get('paheko_API_PASSWORD') -paheko_url=os.environ.get('paheko_url') - -mattermost_user=os.environ.get('mattermost_user') -mattermost_pass=os.environ.get('mattermost_pass') -mattermost_url=os.environ.get('mattermost_url') - -ldap_admin=os.environ.get('ldap_LDAP_ADMIN_USERNAME') -ldap_pass=os.environ.get('ldap_LDAP_ADMIN_PASSWORD') -ldap_root=os.environ.get('ldap_root') -ldap_host="ldapServ.ldapNet" - -#cloud général -cloud_ident=os.environ.get('nextcloud_NEXTCLOUD_ADMIN_USER') -cloud_pass=os.environ.get('nextcloud_NEXTCLOUD_ADMIN_PASSWORD') -cloud_url=os.environ.get('cloud_url') - -gandi_key=os.environ.get('gandi_GANDI_KEY') -gandi_url_api=os.environ.get('gandi_GANDI_API') - -site_url=os.environ.get('site_url') - -#pour webmail_url et mdp_url, ça renvoie des tuples et non des str, bizarre, du coup, je mets en dur -#webmail_url=os.environ.get('webmail_url'), -#mdp_url=os.environ.get('mdp_url'), -webmail_url='https://webmail.kaz.bzh', -mdp_url='https://mdp.kazkouil.fr', #pour le mail app.config['MAIL_SERVER']= os.environ.get('apikaz_MAIL_SERVER') @@ -146,16 +108,6 @@ app.config['MAIL_USE_TLS'] = True app.config['MAIL_USE_SSL'] = False mail = Mail(app) -#sympa -sympa_ident=os.environ.get('sympa_SOAP_USER') -sympa_pass=os.environ.get('sympa_SOAP_PASSWORD') -sympa_url=os.environ.get('sympa_url') -sympa_liste_info=os.environ.get('sympa_liste_info') -MAIL_USERNAME=app.config['MAIL_USERNAME'] - -#pour QUOTA (à virer ensuite) -serveur_imap = os.environ.get('serveur_imap') -mot_de_passe_mail=os.environ.get('mot_de_passe_mail') #************************************************* @app.before_request @@ -257,33 +209,6 @@ def get_token(): #************************************************* #*******MDP*************************************** #************************************************* -class Password_create(Resource): - @jwt_required() - def get(self): - """ - créer un password qui colle avec les appli kaz - --- - tags: - - Password - security: - - Bearer: [] - parameters: [] - responses: - 200: - description: le password - 404: - description: oops - """ - global new_password - - cmd="apg -n 1 -m 10 -M NCL -d" - try: - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - new_password="_"+output.decode("utf-8")+"_" - return new_password,200 - - except subprocess.CalledProcessError as e: - return e.output.decode("utf-8"), 400 api.add_resource(Password_create, '/password/create') @@ -291,1792 +216,76 @@ api.add_resource(Password_create, '/password/create') #*******PAHEKO************************************ #************************************************* -class Paheko_categories(Resource): - - @jwt_required() - def get(self): - """ - Récupérer les catégories Paheko avec le compteur associé - --- - tags: - - Paheko - security: - - Bearer: [] - parameters: [] - responses: - 200: - description: Liste des catégories Paheko - 404: - description: oops - """ - 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 jsonify(data) - else: - return jsonify({'error': 'La requête a échoué'}), response.status_code - api.add_resource(Paheko_categories, '/paheko/user/categories') - -#************************************************* - -class Paheko_users(Resource): - - @jwt_required() - def get(self,categorie): - """ - Afficher les membres d'une catégorie Paheko - --- - tags: - - Paheko - security: - - Bearer: [] - parameters: - - in: path - name: categorie - type: string - required: true - responses: - 200: - description: Liste des membres une catégorie Paheko - 404: - description: oops - """ - - 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 jsonify(data) - else: - return jsonify({'error': 'La requête a échoué'}), response.status_code - api.add_resource(Paheko_users, '/paheko/user/category/') - -#************************************************* - -class Paheko_user(Resource): - - def __init__(self): - global paheko_ident, paheko_pass, paheko_url - self.paheko_ident = paheko_ident - self.paheko_pass = paheko_pass - self.paheko_url = paheko_url - self.auth = (self.paheko_ident, self.paheko_pass) - - @jwt_required() - def get(self,ident): - """ - Afficher un membre de Paheko par son email kaz ou son numéro ou le non court de l'orga - --- - tags: - - Paheko - security: - - Bearer: [] - parameters: - - in: path - name: ident - type: string - required: true - description: possible d'entrer un numéro, un email, le nom court de l'orga - responses: - 200: - description: Existe et affiche - 404: - description: N'existe pas - """ - - 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 jsonify(data["results"][0]) - elif data["count"] == 0: - return "pas de résultat", 400 - else: - return "Plusieurs utilisateurs correspondent ?!", 400 - else: - #return jsonify({'error': 'La requête a échoué'}), response.status_code - return "pas de résultat", response.status_code - -#************************************************* - - @jwt_required() - def put(self,ident,field,new_value): - """ - Modifie la valeur d'un champ d'un membre paheko (ident= numéro paheko ou email kaz) - --- - tags: - - Paheko - security: - - Bearer: [] - parameters: - - in: path - name: ident - type: string - required: true - description: possible d'entrer le numéro paheko, un email kaz - - in: path - name: field - type: string - required: true - description: un champ de la table users de la base paheko - - in: path - name: new_value - type: string - required: true - description: la nouvelle valeur à remplacer - - responses: - 200: - description: Modification effectuée avec succès - 400: - description: Oops, ident non trouvé ou incohérent - 404: - description: Oops, modification du champ KO - """ - - #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: - return "email non trouvé", 400 - elif data['count'] > 1: - return "trop de résultat", 400 - else: - #OK - ident = data['results'][0]['id'] - else: - return "pas de résultat", response.status_code - elif not ident.isdigit(): - return "Identifiant utilisateur invalide", response.status_code - - 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(),response.status_code - -#************************************************* - api.add_resource(Paheko_user, '/paheko/user/', endpoint='paheko_get_user', methods=['GET']) api.add_resource(Paheko_user, '/paheko/user///', endpoint='paheko_maj_user', methods=['PUT']) - -#************************************************* - -class Paheko_users_action(Resource): - - def __init__(self): - global paheko_ident, paheko_pass, paheko_url - self.paheko_ident = paheko_ident - self.paheko_pass = paheko_pass - self.paheko_url = paheko_url - - @jwt_required() - def get(self, action): - """ - retourne tous les membres de paheko avec une action à mener (création du compte kaz / modification...) - --- - tags: - - Paheko - security: - - Bearer: [] - parameters: - - in: path - name: action - type: string - required: true - enum: ['A créer','A modifier','En attente','Aucune'] - responses: - 200: - description: liste des nouveaux kaznautes à créer - 404: - description: aucun nouveau kaznaute à créer - """ - auth = (self.paheko_ident, self.paheko_pass) - - api_url = self.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(),200 - else: - return "pas de résultat", response.status_code - api.add_resource(Paheko_users_action, '/paheko/users/') #************************************************* #*******MATTERMOST******************************** #************************************************* -# on utilise mmctl et pas l'apiv4 de MM -# pourquoi ? passe que mmctl déjà utilisé dans les scripts kaz. - -#************************************************* - -def Mattermost_authenticate(): - # Authentification sur MM - global mattermost_url, mattermost_user, mattermost_pass - cmd = f"/mm/mmctl auth login {mattermost_url} --name local-server --username {mattermost_user} --password {mattermost_pass}" - subprocess.run(cmd, shell=True, stderr=subprocess.STDOUT, check=True) - -#************************************************* - -class Mattermost_message(Resource): - - @jwt_required() - def post(self,message,equipe="kaz",canal="creation-comptes"): - """ - Envoyer un message dans une Equipe/Canal de MM - --- - tags: - - Mattermost - security: - - Bearer: [] - parameters: - - in: path - name: equipe - type: string - required: true - - in: path - name: canal - type: string - required: true - - in: path - name: message - type: string - required: true - - responses: - 200: - description: Affiche un message dans un canal d'une équipe - 500: - description: oops - """ - Mattermost_authenticate() - - try: - cmd="/mm/mmctl post create "+equipe+":"+canal+" --message "+ "\"" + message + "\"" - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - return "Message envoyé", 200 - except subprocess.CalledProcessError: - return "Message non envoyé", 500 - api.add_resource(Mattermost_message, '/mattermost/message///') - -#************************************************* - -class Mattermost_user(Resource): - - def __init__(self): - Mattermost_authenticate() - -#************************************************* - @jwt_required() - def get(self,user): - """ - Le user existe t-il sur MM ? - --- - tags: - - Mattermost User - security: - - Bearer: [] - parameters: - - in: path - name: user - type: string - required: true - description: possible d'entrer un username, un email - responses: - 200: - description: Existe - 404: - description: N'existe pas - """ - - try: - cmd = f"/mm/mmctl user search {user} --json" - user_list_output = subprocess.check_output(cmd, shell=True) - return 200 # Le nom d'utilisateur existe - except subprocess.CalledProcessError: - return 404 # Le nom d'utilisateur n'existe pas - -#************************************************* - @jwt_required() - def post(self,user,email,password): - """ - Créer un utilisateur sur MM - --- - tags: - - Mattermost User - security: - - Bearer: [] - parameters: - - in: path - name: user - type: string - required: true - - in: path - name: email - type: string - required: true - - in: path - name: password - type: string - required: true - responses: - 200: - description: Utilisateur créé - 400: - description: oops, Utilisateur non créé - """ - - # Création de l'utilisateur - try: - cmd = f"/mm/mmctl user create --email {email} --username {user} --password {password}" - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - return output.decode("utf-8"), 200 - - except subprocess.CalledProcessError as e: - return e.output.decode("utf-8"), 400 - -#************************************************* - - @jwt_required() - def delete(self,email): - """ - Supprimer un utilisateur sur MM - --- - tags: - - Mattermost User - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - responses: - 200: - description: Utilisateur supprimé - 400: - description: oops, Utilisateur non supprimé - """ - - try: - cmd = f"/mm/mmctl user delete {email} --confirm" - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - return output.decode("utf-8"), 200 - - except subprocess.CalledProcessError as e: - return e.output.decode("utf-8"), 400 - -#************************************************* - - @jwt_required() - def put(self,email,new_password): - """ - Changer un password pour un utilisateur de MM - --- - tags: - - Mattermost User - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - - in: path - name: new_password - type: string - required: true - responses: - 200: - description: Mot de passe de l'Utilisateur changé - 400: - description: oops, Mot de passe de l'Utilisateur inchangé - """ - try: - cmd = f"/mm/mmctl user change-password {email} --password {new_password}" - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - return output.decode("utf-8"), 200 - - except subprocess.CalledProcessError as e: - return e.output.decode("utf-8"), 400 - -#************************************************* - api.add_resource(Mattermost_user, '/mattermost/user/', endpoint='mattermost_get_user', methods=['GET']) api.add_resource(Mattermost_user, '/mattermost/user/create///', endpoint='mattermost_create_user', methods=['POST']) api.add_resource(Mattermost_user, '/mattermost/user/delete/', endpoint='mattermost_delete_user', methods=['DELETE']) api.add_resource(Mattermost_user, '/mattermost/user/change/password//', endpoint='mattermost_change_user_password', methods=['PUT']) - -#************************************************* - -class Mattermost_user_team(Resource): - - @jwt_required() - def post(self,email,equipe): - """ - Affecte un utilisateur à une équipe MM - --- - tags: - - Mattermost Team - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - - in: path - name: equipe - type: string - required: true - responses: - 200: - description: l'utilisateur a bien été affecté à l'équipe - 400: - description: oops, Utilisateur non affecté - """ - Mattermost_authenticate() - - try: - cmd = f"/mm/mmctl team users add {equipe} {email}" - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - return output.decode("utf-8"), 200 - - except subprocess.CalledProcessError as e: - return e.output.decode("utf-8"), 400 - api.add_resource(Mattermost_user_team, '/mattermost/user/team//') - -#************************************************* - -class Mattermost_user_channel(Resource): - @jwt_required() - def post(self,email,equipe,canal): - """ - Affecte un utilisateur à un canal MM - --- - tags: - - Mattermost - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - - in: path - name: equipe - type: string - required: true - - in: path - name: canal - type: string - required: true - responses: - 200: - description: l'utilisateur a bien été affecté au canal - 400: - description: oops, Utilisateur non affecté - """ - Mattermost_authenticate() - - try: - cmd = f'/mm/mmctl channel users add {equipe}:{canal} {email}' - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - return output.decode("utf-8"), 200 - - except subprocess.CalledProcessError as e: - return e.output.decode("utf-8"), 400 - api.add_resource(Mattermost_user_channel, '/mattermost/user/channel///') - -#************************************************* - -class Mattermost_team(Resource): - - def __init__(self): - Mattermost_authenticate() - -#************************************************* - - @jwt_required() - def get(self): - """ - Lister les équipes sur MM - --- - tags: - - Mattermost Team - security: - - Bearer: [] - parameters: [] - responses: - 200: - description: liste des équipes - 400: - description: oops, Equipe non supprimée - """ - Mattermost_authenticate() - - try: - cmd = f"/mm/mmctl team list --disable-pager" - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - data_list = output.decode("utf-8").strip().split('\n') - data_list.pop() - return data_list, 200 - #return jsonify(data_list),200 - - except subprocess.CalledProcessError as e: - return e.output.decode("utf-8"), 400 - -#************************************************* - @jwt_required() - def post(self,equipe,email): - """ - Créer une équipe sur MM et affecter un admin si email est renseigné (set admin marche pô) - --- - tags: - - Mattermost Team - security: - - Bearer: [] - parameters: - - in: path - name: equipe - type: string - required: true - - in: path - name: email - type: string - required: true - description: admin de l'équipe - responses: - 200: - description: Equipe créée - 400: - description: oops, Equipe non créée - """ - - try: - #DANGER: l'option --email ne rend pas le user admin de l'équipe comme c'est indiqué dans la doc :( - cmd = f"/mm/mmctl team create --name {equipe} --display-name {equipe} --private --email {email}" - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - - #Workaround: on récup l'id du user et de l'équipe pour affecter le rôle "scheme_admin": true, "scheme_user": true avec l'api MM classique. - #TODO: - - return output.decode("utf-8"), 200 - - except subprocess.CalledProcessError as e: - return e.output.decode("utf-8"), 400 - -#************************************************* - @jwt_required() - def delete(self,equipe): - """ - Supprimer une équipe sur MM - --- - tags: - - Mattermost Team - security: - - Bearer: [] - parameters: - - in: path - name: equipe - type: string - required: true - responses: - 200: - description: Equipe supprimée - 400: - description: oops, Equipe non supprimée - """ - Mattermost_authenticate() - - try: - cmd = f"/mm/mmctl team delete {equipe} --confirm" - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - return output.decode("utf-8"), 200 - - except subprocess.CalledProcessError as e: - return e.output.decode("utf-8"), 400 - -#************************************************* api.add_resource(Mattermost_team, '/mattermost/team/list',endpoint='mattermost_team_list', methods=['GET']) api.add_resource(Mattermost_team, '/mattermost/team/create//',endpoint='mattermost_team_create', methods=['POST']) api.add_resource(Mattermost_team, '/mattermost/team/delete/',endpoint='mattermost_team_delete', methods=['DELETE']) + #************************************************* #***** LDAP ************************************** #************************************************* -class Ldap_user(Resource): - - def __init__(self): - global ldap_admin, ldap_pass, ldap_root, ldap_host - self.ldap_admin = ldap_admin - self.ldap_pass = ldap_pass - self.ldap_root = ldap_root - self.ldap_host = f"ldap://{ldap_host}" - - def _connect_ldap(self): - ldap_connection = ldap.initialize(self.ldap_host) - ldap_connection.simple_bind_s("cn={},{}".format(self.ldap_admin, self.ldap_root), self.ldap_pass) - return ldap_connection - - @classmethod - def is_valid_field(cls, field): - allowed_fields = ['mailDeSecours', 'mailEnabled', 'nextcloudEnabled', 'mobilizonEnabled', 'agoraEnabled', 'userPassword', 'identifiantKaz', 'mailAlias', 'quota'] - return field in allowed_fields - - @jwt_required() - def get(self, email): - """ - Vérifier si un utilisateur avec cet email existe dans le LDAP soit comme mail principal soit comme alias - --- - tags: - - Ldap - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - responses: - 200: - description: Existe - 400: - description: N'existe pas - 401: - description: oops, email invalide - 402: - description: oops, autre erreur - - """ - try: - - if not validate_email(email): - return "Adresse e-mail non valide", 400 - - ldap_connection = self._connect_ldap() - - #result = ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, "(cn={})".format(email)) - - # Créer une chaîne de filtre pour rechercher dans les champs "cn" et "mailAlias" - filter_str = "(|(cn={})(mailAlias={}))".format(email, email) - result = ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, filter_str) - - ldap_connection.unbind_s() - - if result: - return True, 200 - else: - return False, 400 - - except EmailNotValidError as e: - return str(e), 401 - - except ldap.LDAPError as e: - return str(e), 402 - - -#************************************************* - @jwt_required() - def delete(self, email): - """ - Supprimer un utilisateur du LDAP par son adresse e-mail - --- - tags: - - Ldap - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - responses: - 200: - description: Utilisateur supprimé avec succès - 404: - description: Utilisateur non trouvé dans le LDAP - 400: - description: Erreur lors de la suppression de l'utilisateur - """ - try: - if not validate_email(email): - return "Adresse e-mail non valide", 400 - - ldap_connection = self._connect_ldap() - - # Recherche de l'utilisateur - result = ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, "(cn={})".format(email)) - - if not result: - return False, 404 # Utilisateur non trouvé - - # Récupération du DN de l'utilisateur - dn = result[0][0] - - # Suppression de l'utilisateur - ldap_connection.delete_s(dn) - ldap_connection.unbind_s() - - return True, 200 # Utilisateur supprimé avec succès - - except ldap.NO_SUCH_OBJECT: - return False, 404 # Utilisateur non trouvé - except ldap.LDAPError as e: - return str(e), 400 # Erreur lors de la suppression - except EmailNotValidError as e: - return str(e), 400 - - -#************************************************* - @jwt_required() - def post(self, email): - """ - Ajouter, supprimer ou modifier un champ pour l'utilisateur LDAP - --- - tags: - - Ldap - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - - in: query - name: action - type: string - required: true - enum: ['add', 'delete', 'modify'] - - in: body - name: data - required: true - schema: - type: object - properties: - field: - type: string - enum: ['mailDeSecours', 'mailEnabled', 'nextcloudEnabled', 'mobilizonEnabled', 'agoraEnabled', 'userPassword', 'identifiantKaz', 'mailAlias', 'quota'] - description: Le champ à ajouter, supprimer ou modifier (par exemple, mailDeSecours, mailAlias, etc.) - value: - type: string - description: La valeur à ajouter, supprimer ou modifier pour le champ spécifié - responses: - 200: - description: Opération réussie - 404: - description: Utilisateur non trouvé dans le LDAP - 400: - description: Erreur lors de l'opération - """ - - try: - if not validate_email(email): - return "Adresse e-mail non valide", 400 - - action = request.args.get('action') - field = request.json.get('field') - value = request.json.get('value') - - if not action or not field or not value: - return "Action, champ ou valeur manquant", 400 - - if not self.is_valid_field(field): - return "Champ non autorisé", 400 - - ldap_connection = self._connect_ldap() - result = ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, "(cn={})".format(email)) - - if not result: - return False, 404 - - dn = result[0][0] - - if field == 'userPassword' and (action == 'add' or action == 'modify'): - password_chiffre = sha512_crypt.hash(value) - value = "{{CRYPT}}{}".format(password_chiffre) - - if action == 'add': - mod_attrs = [(ldap.MOD_ADD, field, value.encode('utf-8'))] - - elif action == 'delete': - mod_attrs = [(ldap.MOD_DELETE, field, value.encode('utf-8'))] - - elif action == 'modify': - - if field == 'quota': - mail_quota_value = value + 'G' - nextcloud_quota_value = value + " GB" - mod_attrs = [ - (ldap.MOD_REPLACE, 'quota', value.encode('utf-8')), - (ldap.MOD_REPLACE, 'mailQuota', mail_quota_value.encode('utf-8')), - (ldap.MOD_REPLACE, 'nextcloudQuota', nextcloud_quota_value.encode('utf-8')) - ] - else: - mod_attrs = [(ldap.MOD_REPLACE, field, value.encode('utf-8'))] - - else: - return "Action non valide", 400 - - ldap_connection.modify_s(dn, mod_attrs) - ldap_connection.unbind_s() - - return True, 200 - - except ldap.NO_SUCH_OBJECT: - return False, 404 - except ldap.LDAPError as e: - return str(e), 400 - except EmailNotValidError as e: - return str(e), 400 - -#************************************************* - @jwt_required() - def put(self, email, **kwargs): - """ - 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 ? - --- - tags: - - Ldap - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - - in: body - name: data - required: true - schema: - type: object - properties: - prenom: - type: string - description: Prénom de l'utilisateur - nom: - type: string - description: Nom de l'utilisateur - password: - type: string - description: Mot de passe de l'utilisateur - email_secours: - type: string - description: Adresse e-mail de secours - quota: - type: string - description: Quota de l'utilisateur - responses: - 200: - description: Utilisateur ajouté avec succès - 400: - description: Erreur lors de l'ajout de l'utilisateur - 406: - description: Erreur utilisateur déjà existant - - """ - try: - - if kwargs: # appel depuis une autre api - email_secours = kwargs.get('email_secours') - prenom = kwargs.get('prenom') - nom = kwargs.get('nom') - password = kwargs.get('password') - quota = kwargs.get('quota') - - else: # appel depuis swagger - email_secours = request.json.get('email_secours') - nom = request.json.get('nom') - prenom = request.json.get('prenom') - password = request.json.get('password') - quota = request.json.get('quota') - - password_chiffre = sha512_crypt.hash(password) - - if not validate_email(email) or not validate_email(email_secours): - return "Adresse e-mail ou secours non valide", 400 - - #le user existe t-il déjà ? - ldap_connection = self._connect_ldap() - result = ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, "(cn={})".format(email)) - - if result: - return "User déjà existant", 406 - - # Construire le DN - dn = f"cn={email},ou=users,{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')) - ] - - ldap_connection.add_s(dn, mod_attrs) - ldap_connection.unbind_s() - - return "Utilisateur créé dand le ldap", 200 - - except ldap.LDAPError as e: - return str(e), 400 - except EmailNotValidError as e: - return str(e), 400 - -#************************************************* - -# Définition des routes avec les méthodes correspondantes api.add_resource(Ldap_user, '/ldap/user/', endpoint='ldap_user_get', methods=['GET']) api.add_resource(Ldap_user, '/ldap/user/delete/', endpoint='ldap_user_delete', methods=['DELETE']) api.add_resource(Ldap_user, '/ldap/user/change/', endpoint='ldap_user_change', methods=['POST']) api.add_resource(Ldap_user, '/ldap/user/add/', endpoint='ldap_user_add', methods=['PUT']) - #************************************************* #***** CLOUD ************************************** #************************************************* -#TODO: pas réussi à faire une seule classe Cloud_user avec 2 méthodes get/delete -class Cloud_user(Resource): - - @jwt_required() - def get(self, email): - """ - Existe dans le cloud général ? - --- - tags: - - Cloud - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - responses: - 200: - description: Succès - 404: - description: L'utilisateur n'existe pas - 500: - description: Erreur interne du serveur - """ - - global cloud_ident, cloud_pass, cloud_url - - try: - auth = (cloud_ident, cloud_pass) - api_url = f"{cloud_url}/ocs/v1.php/cloud/users?search={email}" - headers = {"OCS-APIRequest": "true"} - response = requests.get(api_url, auth=auth, headers=headers) - - if response.status_code == 200: - if re.search(r'.*', response.text): - return 200 - else: - return 404 - - except Exception as e: - return jsonify({'error': str(e)}), 500 - api.add_resource(Cloud_user, '/cloud/user/') - -#************************************************* -class Cloud_user_delete(Resource): - - @jwt_required() - def delete(self, email): - """ - Supprime le compte dans le cloud général - QUESTION: A PRIORI INUTILE CAR LIE AU LDAP - --- - tags: - - Cloud - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - responses: - 200: - description: Succès, l'utilisateur a été supprimé du cloud général - 404: - description: Oops, l'utilisateur n'a pas été supprimé du cloud général - 500: - description: Erreur interne du serveur - """ - - global cloud_ident, cloud_pass, cloud_url - - try: - auth = (cloud_ident, cloud_pass) - api_url = f"{cloud_url}/ocs/v1.php/cloud/users?search={email}" - headers = {"OCS-APIRequest": "true"} - response = requests.delete(api_url, auth=auth, headers=headers) - - if response.status_code == 200: - if re.search(r'.*', response.text): - return 200 - else: - return 404 - - except Exception as e: - return jsonify({'error': str(e)}), 500 - api.add_resource(Cloud_user_delete, '/cloud/user/delete/') - -#************************************************* -# class Cloud_user_change(Resource): -# @jwt_required() -# def put(self, email, new_password): -# """ -# Modifie le mot de passe d'un Utilisateur dans le cloud général: QUESTION: A PRIORI INUTILE CAR LIE AU LDAP -# --- -# tags: -# - Cloud -# security: -# - Bearer: [] -# parameters: -# - in: path -# name: email -# type: string -# required: true -# - in: path -# name: new_password -# type: string -# required: true -# responses: -# 200: -# description: Succès, mot de passe changé -# 404: -# description: Oops, mot de passe NON changé -# 500: -# description: Erreur interne du serveur -# """ -# -# global cloud_ident, cloud_pass, cloud_url -# -# try: -# auth = (cloud_ident, cloud_pass) -# api_url = f"{cloud_url}/ocs/v1.php/cloud/users?search={email}" -# headers = {"OCS-APIRequest": "true"} -# data = { -# "key": "password", -# "value": new_password -# } -# response = requests.put(api_url, auth=auth, headers=headers) -# -# if response.status_code == 200: -# if re.search(r'.*', response.text): -# return 200 -# else: -# return 404 -# -# except Exception as e: -# return jsonify({'error': str(e)}), 500 -# # api.add_resource(Cloud_user_change, '/cloud/user/change//') #************************************************* #***** SYMPA ************************************** #************************************************* -class Sympa_user(Resource): - def __init__(self): - global sympa_ident, sympa_pass, sympa_url,MAIL_USERNAME - self.sympa_ident = sympa_ident - self.sympa_pass = sympa_pass - self.sympa_url = sympa_url - - def _execute_sympa_command(self, email, liste, service): - try: - if validate_email(email) and validate_email(liste): - cmd = f'export PERL5LIB=/usr/src/app/:$PERL5LIB && /usr/src/app/Sympa/sympa_soap_client.pl --soap_url={self.sympa_url}/sympasoap --trusted_application={self.sympa_ident} --trusted_application_password={self.sympa_pass} --proxy_vars=USER_EMAIL={MAIL_USERNAME} --service={service} --service_parameters="{liste},{email}" && echo $?' - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - return output.decode("utf-8"), 200 # Retourne la sortie et un code de succès - except EmailNotValidError as e: - return str(e), 400 # Retourne le message d'erreur et un code d'erreur 400 - except subprocess.CalledProcessError as e: - return e.output.decode("utf-8"), 400 # Retourne la sortie de la commande et un code d'erreur 400 - - @jwt_required() - def post(self, email, liste): - """ - Ajouter un email dans une liste sympa - --- - tags: - - Sympa - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - - in: path - name: liste - type: string - required: true - responses: - 200: - description: Succès, email ajouté dans la liste - 400: - description: Oops, email non ajouté dans la liste - """ - output, status_code = self._execute_sympa_command(email, liste, 'add') - return output, status_code - - @jwt_required() - def delete(self, email, liste): - """ - Supprimer un email dans une liste sympa - --- - tags: - - Sympa - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - - in: path - name: liste - type: string - required: true - responses: - 200: - description: Succès, email supprimé dans la liste - 400: - description: Oops, email non supprimé dans la liste - """ - output, status_code = self._execute_sympa_command(email, liste, 'del') - return output, status_code - api.add_resource(Sympa_user, '/sympa/user//') #************************************************* #***** QUOTA ************************************** #************************************************* -class Quota(Resource): - -#https://doc.dovecot.org/configuration_manual/authentication/master_users/ - -#https://blog.debugo.fr/serveur-messagerie-dovecot/ -# sur kazkouil.fr, j'ai modifié /etc/dovecot/conf.d/20-lmtp.conf -#mail_plugins = $mail_plugins sieve quota - - @jwt_required() - def get(self, email): - """ - Récupérer la place prise par une BAL (EN COURS) - --- - tags: - - Quota - security: - - Bearer: [] - parameters: - - in: path - name: email - type: string - required: true - responses: - 200: - description: Succès, taille d'une BAL' - 400: - description: Oops, échec dans l'affichage de la taille d'une BAL - """ - global serveur_imap, mot_de_passe_mail - - try: - if validate_email(email): - # Connexion au serveur IMAP - mail = imaplib.IMAP4_SSL(serveur_imap) - mail.login(email, mot_de_passe_mail) - - #res, data = mail.select("INBOX") - #return data[0].decode("utf-8"), 200 - - # Requête pour obtenir le quota de la boîte aux lettres - # res, data = mail.getquota("INBOX") - # return str(data[0]).split() - - res, data = mail.getquotaroot("INBOX") - return str(data[1]).split()[3]+" KB" - - # Fermeture de la connexion - mail.close() - mail.logout() - - else: - return "Email non valide", 400 - - except imaplib.IMAP4.error as e: - return str(e), 400 # Retourne le message d'erreur et un code d'erreur 400 - except EmailNotValidError as e: - return str(e), 400 # Retourne le message d'erreur et un code d'erreur 400 - -api.add_resource(Quota, '/quota/') - +api.add_resource(Quota, '/quota/') #************************************************* #***** DNS ************************************** #************************************************* -class Dns_serveurs(Resource): - - def __init__(self): - global gandi_key, gandi_url_api - self.gandi_key = gandi_key - self.gandi_url_api = gandi_url_api - - @jwt_required() - def get(self): - """ - Renvoie tous les serveurs kaz de la zone dns - --- - tags: - - Dns - security: - - Bearer: [] - responses: - 200: - description: Succès, liste des serveurs - 404: - description: Oops, soucis... - """ - url = f"{self.gandi_url_api}/records/srv/TXT" - headers = { "Authorization": f"Apikey {self.gandi_key}" } - response = requests.get(url, headers=headers) - - if response.status_code != 200: - return response.json(),response.status_code - - # Extraire la liste des serveurs de la réponse JSON - rrset_values = response.json()["rrset_values"] - # Nettoyer la liste - serveurs = [serveur.strip('"') for serveur in rrset_values[0].split(";")] - return serveurs, 200 - -api.add_resource(Dns_serveurs, '/dns/') - -#************************************************* - -class Dns(Resource): - - def __init__(self): - global gandi_key, gandi_url_api - self.gandi_key = gandi_key - self.gandi_url_api = gandi_url_api - self.dns_serveurs_resource = Dns_serveurs() - -#************************************************* - @jwt_required() - def get(self,sdomaine): - """ - Le sous-domaine existe t-il dans la zone dns avec un enreg CNAME ? - --- - tags: - - Dns - security: - - Bearer: [] - parameters: - - in: path - name: sdomaine - type: string - required: true - responses: - 200: - description: Succès, sdomaine existe déjà avec un CNAME - 404: - description: Oops, sdomaine non trouvé - """ - url = f"{self.gandi_url_api}/records/{sdomaine}/CNAME" - headers = { "Authorization": f"Apikey {self.gandi_key}" } - response = requests.get(url, headers=headers) - return response.json(),response.status_code - - -#************************************************* - - @jwt_required() - def delete(self,sdomaine): - """ - suppression du sdomaine - --- - tags: - - Dns - security: - - Bearer: [] - parameters: - - in: path - name: sdomaine - type: string - required: true - responses: - 204: - description: Succès, sdomaine supprimé - 404: - description: Oops, - """ - url = f"{self.gandi_url_api}/records/{sdomaine}" - headers = { "Authorization": f"Apikey {self.gandi_key}" } - response = requests.delete(url, headers=headers) - return response.text,response.status_code - -#************************************************* - - @jwt_required() - def post(self,sdomaine,serveur): - """ - Créé le sous-domaine de type CNAME qui pointe sur serveur - --- - tags: - - Dns - security: - - Bearer: [] - parameters: - - in: path - name: sdomaine - type: string - required: true - - in: path - name: serveur - type: string - required: true - description: Le serveur doit être l'un des serveurs disponibles dans la zone DNS - responses: - 201: - description: Succès, sdomaine créé comme CNAME - 400: - description: Oops, serveur inconnu, sdomaine non créé - 404: - description: Oops, sdomaine non créé - 200,409: - description: Oops, sdomaine déjà créé - """ - - - # Récupérer la liste des serveurs disponibles - serveurs_disponibles, status_code = self.dns_serveurs_resource.get() - - if status_code != 200: - return serveurs_disponibles, status_code - - if serveur not in serveurs_disponibles: - return f"Erreur: Le serveur {serveur} n'est pas disponible", 400 - - url = f"{self.gandi_url_api}/records/{sdomaine}/CNAME" - headers = { "Authorization": f"Apikey {self.gandi_key}" } - payload = f'{{"rrset_values":["{serveur}"]}}' - - response = requests.post(url, data=payload, headers=headers) - return response.json(),response.status_code - -#************************************************* - api.add_resource(Dns, '/dns/', endpoint='dns_get', methods=['GET']) api.add_resource(Dns, '/dns/', endpoint='dns_delete', methods=['DELETE']) api.add_resource(Dns, '/dns//', endpoint='dns_post', methods=['POST']) - -#************************************************* +api.add_resource(Dns_serveurs, '/dns/') #************************************************* #***** KAZ ************************************** #************************************************* -class Kaz_user(Resource): - - def __init__(self): - 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() - - -#******************************************************************************************** - @jwt_required() - def delete(self): - """ - Utile pour les tests de createUser. Avant le POST de /kaz/create/users. Ça permet de supprimer/maj les comptes. - --- - tags: - - Kaz User - security: - - Bearer: [] - parameters: [] - responses: - 201: - description: Succès - 401: - description: Oops, un soucis quelconque - """ - #verrou pour empêcher de lancer en même temps la même api - try: - prefixe="del_user_lock_" - if glob(f"{tempfile.gettempdir()}/{prefixe}*"): raise ValueError("ERREUR 0 : api déjà en cours d'utilisation !") - lock_file = tempfile.NamedTemporaryFile(prefix=prefixe,delete=True) - - #TODO à remplir à la main - liste_emails=["",""] - email_secours="" - liste_sympa="" - - for email in liste_emails: - res, status_code = self.ldap_user_resource.delete(email) - res, status_code = self.mattermost_user_resource.delete(email) - nom_orga=''.join(random.choice(string.ascii_lowercase) for _ in range(8)) - res, status_code = self.paheko_user_resource.put(email,"nom_orga",nom_orga) - res, status_code = self.paheko_user_resource.put(email,"action_auto","A créer") - res, status_code = self.paheko_user_resource.put(email,"email_secours",email_secours) - res, status_code = self.sympa_user_resource.delete(email,liste_sympa) - res, status_code = self.sympa_user_resource.delete(email_secours,liste_sympa) - msg=f"**POST AUTO** suppression de {email} ok" - self.mattermost_message_resource.post(message=msg) - return "OK", 200 - - except ValueError as e: - msg=f"(classe: {__class__.__name__} : {e}" - self.mattermost_message_resource.post(message=msg) - return str(msg), 401 - -#******************************************************************************************** - - @jwt_required() - def post(self): - """ - Créé un nouveau kaznaute: inscription sur MM / Cloud / email + msg sur MM + email à partir de action="a créer" sur paheko - --- - tags: - - Kaz User - security: - - Bearer: [] - parameters: [] - responses: - 201: - description: Succès, kaznaute créé - 400: - description: Oops, rien à créer - 401: - description: Oops, un soucis quelconque - """ - - try: - - #verrou pour empêcher de lancer en même temps la même api - prefixe="create_user_lock_" - if glob(f"{tempfile.gettempdir()}/{prefixe}*"): raise ValueError("ERREUR 0 : api déjà en cours d'utilisation !") - lock_file = tempfile.NamedTemporaryFile(prefix=prefixe,delete=True) - - #qui sont les kaznautes à créer ? - liste_kaznautes, status_code = self.paheko_users_action_resource.get("A créer") - - if liste_kaznautes=="pas de résultat": return "ERREUR: paheko non joignable",401 - - count=liste_kaznautes['count'] - if count==0: return "aucun nouveau kaznaute à créer",400 - - #au moins un kaznaute à créer - for tab in liste_kaznautes['results']: - email = tab['email'].lower() - - # est-il déjà dans le ldap ? (mail ou alias) - res, status_code = self.ldap_user_resource.get(email) - if status_code != 400: raise ValueError(f"ERREUR 1: {email} déjà existant dans ldap. {res}, on arrête tout") - - #test nom orga - if tab['admin_orga'] == 1: - if tab['nom_orga'] is None: - raise ValueError(f"ERREUR 0 sur paheko: {email} : nom_orga vide, on arrête tout") - if not bool(re.match(r'^[a-z0-9-]+$', tab['nom_orga'])): - raise ValueError(f"ERREUR 0 sur paheko: {email} : nom_orga ({tab['nom_orga']}) incohérent (minuscule/chiffre/-), on arrête tout") - - #test email_secours - email_secours = tab['email_secours'].lower() - if not validate_email(email_secours): raise EmailNotValidError() - - #test quota - quota = tab['quota_disque'] - if not quota.isdigit(): raise ValueError(f"ERREUR 2: quota non numérique : {quota}, on arrête tout") - - #quel type de test ? - #"nom": "ROUSSEAU Mickael", - nom, prenom = tab['nom'].split(maxsplit=1) - - #on génère un password - password,status_code = self.password_create_resource.get() - - #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 - } - res, status_code = self.ldap_user_resource.put(email, **data) - if status_code != 200: raise ValueError(f"ERREUR 3 sur ldap: {email} : {res}, on arrête tout ") - - #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) - - return "fin des inscriptions", 201 - - except EmailNotValidError as e: - msg=f"classe: {__class__.__name__} : ERREUR 13 : email_secours : {email_secours} " + str(e) +", on arrête tout" - self.mattermost_message_resource.post(message=msg) - return msg, 401 - except ValueError as e: - msg=f"(classe: {__class__.__name__} : {e}" - self.mattermost_message_resource.post(message=msg) - return str(msg), 401 - - -#************************************************* - api.add_resource(Kaz_user, '/kaz/create/users', endpoint='kaz_create_user', methods=['POST']) api.add_resource(Kaz_user, '/kaz/delete/user', endpoint='kaz_delete_user', methods=['DELETE']) #************************************************* #**********TEST*********************************** #************************************************* -class Test(Resource): - - def __init__(self): - toto="toto" - #self.mattermost_team_resource=Mattermost_team() - #global mattermost_url, sympa_url, webmail_url, mdp_url, site_url, nc_url - -#******************************************************************************************** - @jwt_required() - def get(self): - """ - Pour tester des conneries: # test lançement de cmde ssh sur des serveurs distants: - --- - tags: - - Test - security: - - Bearer: [] - parameters: [] - responses: - 201: - description: OK - 401: - description: KO - """ -#******************************************************************************************** - - -# test lançcement de cmde ssh sur des serveurs distants: -# il faut au préalable que la clé publique de root du conteneur apikaz soit dans authorized key du user fabricer de la machine 163.172.94.54 -# clé à créer dans le Dockerfile -# risque sécu ? - - - cmd="ssh -p 2201 fabricer@163.172.94.54 mkdir -p /tmp/toto" - try: - output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) - return "ok",200 - - except subprocess.CalledProcessError as e: - return e.output.decode("utf-8"), 400 - -#******************************************************************************************** -# #***** test suppression de toutes les équipes de MM sauf KAZ -# res,status=self.mattermost_team_resource=Mattermost_team().get() -# for equipe in res: -# if equipe!="kaz": res,status=self.mattermost_team_resource=Mattermost_team().delete(equipe) -# return "fin" - -#******************************************************************************************** - -#**** test messagerie - -# NOM="toto" -# EMAIL_SOUHAITE='f@kaz.bzh' -# PASSWORD="toto" -# QUOTA="1" -# ADMIN_ORGA="0" -# -# context = { -# 'ADMIN_ORGA': ADMIN_ORGA, -# 'NOM': NOM, -# 'EMAIL_SOUHAITE': EMAIL_SOUHAITE, -# 'PASSWORD': PASSWORD, -# 'QUOTA': QUOTA, -# '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_SOUHAITE]) -# msg.html = render_template('email_inscription.html', **context) -# -# # Parsez le contenu HTML avec BeautifulSoup -# soup = BeautifulSoup(msg.html, 'html.parser') -# msg.body = soup.get_text() -# -# mail.send(msg) -# return "Message envoyé!" - -#******************************************************************************************** -# #**** test ms erreur -# email_secours="toto" -# -# msg=f"classe: {__class__.__name__} : ERREUR 8 : email_secours : {email_secours} " +", on arrête tout" -# #return __class__.__name__ -# return msg - -#******************************************************************************************** -#**** test vérou -# prefixe="toto_" -# if glob(f"{tempfile.gettempdir()}/{prefixe}*"): -# return "ERREUR : api déjà en cours d'utilisation !", 400 -# else: -# lock_file = tempfile.NamedTemporaryFile(prefix=prefixe,delete=True) -# -# sleep(20) -# return str(lock_file), 201 api.add_resource(Test, '/test', endpoint='test', methods=['GET']) diff --git a/dockers/apikaz/source/resources/cloud.py b/dockers/apikaz/source/resources/cloud.py new file mode 100644 index 0000000..f734734 --- /dev/null +++ b/dockers/apikaz/source/resources/cloud.py @@ -0,0 +1,147 @@ + +from resources.common_imports import * + +#les variables globales minimum +from resources.config import cloud_ident, cloud_pass, cloud_url + + +#TODO: pas réussi à faire une seule classe Cloud_user avec 2 méthodes get/delete + +class Cloud_user(Resource): + + @jwt_required() + def get(self, email): + """ + Existe dans le cloud général ? + --- + tags: + - Cloud + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + responses: + 200: + description: Succès + 404: + description: L'utilisateur n'existe pas + 500: + description: Erreur interne du serveur + """ + + global cloud_ident, cloud_pass, cloud_url + + try: + auth = (cloud_ident, cloud_pass) + api_url = f"{cloud_url}/ocs/v1.php/cloud/users?search={email}" + headers = {"OCS-APIRequest": "true"} + response = requests.get(api_url, auth=auth, headers=headers) + + if response.status_code == 200: + if re.search(r'.*', response.text): + return 200 + else: + return 404 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +#************************************************* +class Cloud_user_delete(Resource): + + @jwt_required() + def delete(self, email): + """ + Supprime le compte dans le cloud général + QUESTION: A PRIORI INUTILE CAR LIE AU LDAP + --- + tags: + - Cloud + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + responses: + 200: + description: Succès, l'utilisateur a été supprimé du cloud général + 404: + description: Oops, l'utilisateur n'a pas été supprimé du cloud général + 500: + description: Erreur interne du serveur + """ + + global cloud_ident, cloud_pass, cloud_url + + try: + auth = (cloud_ident, cloud_pass) + api_url = f"{cloud_url}/ocs/v1.php/cloud/users?search={email}" + headers = {"OCS-APIRequest": "true"} + response = requests.delete(api_url, auth=auth, headers=headers) + + if response.status_code == 200: + if re.search(r'.*', response.text): + return 200 + else: + return 404 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +#************************************************* +# class Cloud_user_change(Resource): +# @jwt_required() +# def put(self, email, new_password): +# """ +# Modifie le mot de passe d'un Utilisateur dans le cloud général: QUESTION: A PRIORI INUTILE CAR LIE AU LDAP +# --- +# tags: +# - Cloud +# security: +# - Bearer: [] +# parameters: +# - in: path +# name: email +# type: string +# required: true +# - in: path +# name: new_password +# type: string +# required: true +# responses: +# 200: +# description: Succès, mot de passe changé +# 404: +# description: Oops, mot de passe NON changé +# 500: +# description: Erreur interne du serveur +# """ +# +# global cloud_ident, cloud_pass, cloud_url +# +# try: +# auth = (cloud_ident, cloud_pass) +# api_url = f"{cloud_url}/ocs/v1.php/cloud/users?search={email}" +# headers = {"OCS-APIRequest": "true"} +# data = { +# "key": "password", +# "value": new_password +# } +# response = requests.put(api_url, auth=auth, headers=headers) +# +# if response.status_code == 200: +# if re.search(r'.*', response.text): +# return 200 +# else: +# return 404 +# +# except Exception as e: +# return jsonify({'error': str(e)}), 500 +# diff --git a/dockers/apikaz/source/resources/common_imports.py b/dockers/apikaz/source/resources/common_imports.py new file mode 100644 index 0000000..fb7d1ac --- /dev/null +++ b/dockers/apikaz/source/resources/common_imports.py @@ -0,0 +1,25 @@ +import os +import re +import requests +import subprocess +import logging +import tempfile +import ldap +import imaplib +import random +import string +import json + +from flask import Flask, jsonify, send_from_directory, request, abort, json, Response, render_template +from flask_mail import Mail, Message +from flasgger import Swagger +from flask_restful import Api, Resource +from flask_jwt_extended import JWTManager, create_access_token, jwt_required + +from passlib.hash import sha512_crypt +from unidecode import unidecode +from email_validator import validate_email, EmailNotValidError +from time import sleep +from glob import glob +from bs4 import BeautifulSoup +from datetime import datetime diff --git a/dockers/apikaz/source/resources/config.py b/dockers/apikaz/source/resources/config.py new file mode 100644 index 0000000..be4dc96 --- /dev/null +++ b/dockers/apikaz/source/resources/config.py @@ -0,0 +1,45 @@ +import os + +#le paheko de kaz +paheko_ident=os.environ.get('paheko_API_USER') +paheko_pass=os.environ.get('paheko_API_PASSWORD') +paheko_url=os.environ.get('paheko_url') + +#mattermost +mattermost_user=os.environ.get('mattermost_user') +mattermost_pass=os.environ.get('mattermost_pass') +mattermost_url=os.environ.get('mattermost_url') + +#ldap +ldap_admin=os.environ.get('ldap_LDAP_ADMIN_USERNAME') +ldap_pass=os.environ.get('ldap_LDAP_ADMIN_PASSWORD') +ldap_root=os.environ.get('ldap_root') +ldap_host="ldapServ.ldapNet" + +#cloud général +cloud_ident=os.environ.get('nextcloud_NEXTCLOUD_ADMIN_USER') +cloud_pass=os.environ.get('nextcloud_NEXTCLOUD_ADMIN_PASSWORD') +cloud_url=os.environ.get('cloud_url') + +#sympa +sympa_ident=os.environ.get('sympa_SOAP_USER') +sympa_pass=os.environ.get('sympa_SOAP_PASSWORD') +sympa_url=os.environ.get('sympa_url') +sympa_liste_info=os.environ.get('sympa_liste_info') +MAIL_USERNAME=os.environ.get('apikaz_MAIL_USERNAME') + +#pour QUOTA (à virer ensuite) +serveur_imap = os.environ.get('serveur_imap') +mot_de_passe_mail=os.environ.get('mot_de_passe_mail') + +#dns +gandi_key=os.environ.get('gandi_GANDI_KEY') +gandi_url_api=os.environ.get('gandi_GANDI_API') + +#kaz_user +site_url=os.environ.get('site_url') +#pour webmail_url et mdp_url, ça renvoie des tuples et non des str, bizarre, du coup, je mets en dur +#webmail_url=os.environ.get('webmail_url'), +#mdp_url=os.environ.get('mdp_url'), +webmail_url='https://webmail.kaz.bzh', +mdp_url='https://mdp.kazkouil.fr', diff --git a/dockers/apikaz/source/resources/dns.py b/dockers/apikaz/source/resources/dns.py new file mode 100644 index 0000000..04dbd3e --- /dev/null +++ b/dockers/apikaz/source/resources/dns.py @@ -0,0 +1,159 @@ +from resources.common_imports import * + +#les variables globales minimum +from resources.config import gandi_key, gandi_url_api + + +class Dns_serveurs(Resource): + + def __init__(self): + global gandi_key, gandi_url_api + self.gandi_key = gandi_key + self.gandi_url_api = gandi_url_api + + @jwt_required() + def get(self): + """ + Renvoie tous les serveurs kaz de la zone dns + --- + tags: + - Dns + security: + - Bearer: [] + responses: + 200: + description: Succès, liste des serveurs + 404: + description: Oops, soucis... + """ + url = f"{self.gandi_url_api}/records/srv/TXT" + headers = { "Authorization": f"Apikey {self.gandi_key}" } + response = requests.get(url, headers=headers) + + if response.status_code != 200: + return response.json(),response.status_code + + # Extraire la liste des serveurs de la réponse JSON + rrset_values = response.json()["rrset_values"] + # Nettoyer la liste + serveurs = [serveur.strip('"') for serveur in rrset_values[0].split(";")] + return serveurs, 200 + + +#************************************************* + +class Dns(Resource): + + def __init__(self): + global gandi_key, gandi_url_api + self.gandi_key = gandi_key + self.gandi_url_api = gandi_url_api + self.dns_serveurs_resource = Dns_serveurs() + +#************************************************* + @jwt_required() + def get(self,sdomaine): + """ + Le sous-domaine existe t-il dans la zone dns avec un enreg CNAME ? + --- + tags: + - Dns + security: + - Bearer: [] + parameters: + - in: path + name: sdomaine + type: string + required: true + responses: + 200: + description: Succès, sdomaine existe déjà avec un CNAME + 404: + description: Oops, sdomaine non trouvé + """ + url = f"{self.gandi_url_api}/records/{sdomaine}/CNAME" + headers = { "Authorization": f"Apikey {self.gandi_key}" } + response = requests.get(url, headers=headers) + return response.json(),response.status_code + + +#************************************************* + + @jwt_required() + def delete(self,sdomaine): + """ + suppression du sdomaine + --- + tags: + - Dns + security: + - Bearer: [] + parameters: + - in: path + name: sdomaine + type: string + required: true + responses: + 204: + description: Succès, sdomaine supprimé + 404: + description: Oops, + """ + url = f"{self.gandi_url_api}/records/{sdomaine}" + headers = { "Authorization": f"Apikey {self.gandi_key}" } + response = requests.delete(url, headers=headers) + return response.text,response.status_code + +#************************************************* + + @jwt_required() + def post(self,sdomaine,serveur): + """ + Créé le sous-domaine de type CNAME qui pointe sur serveur + --- + tags: + - Dns + security: + - Bearer: [] + parameters: + - in: path + name: sdomaine + type: string + required: true + - in: path + name: serveur + type: string + required: true + description: Le serveur doit être l'un des serveurs disponibles dans la zone DNS + responses: + 201: + description: Succès, sdomaine créé comme CNAME + 400: + description: Oops, serveur inconnu, sdomaine non créé + 404: + description: Oops, sdomaine non créé + 200,409: + description: Oops, sdomaine déjà créé + """ + + + # Récupérer la liste des serveurs disponibles + serveurs_disponibles, status_code = self.dns_serveurs_resource.get() + + if status_code != 200: + return serveurs_disponibles, status_code + + if serveur not in serveurs_disponibles: + return f"Erreur: Le serveur {serveur} n'est pas disponible", 400 + + url = f"{self.gandi_url_api}/records/{sdomaine}/CNAME" + headers = { "Authorization": f"Apikey {self.gandi_key}" } + payload = f'{{"rrset_values":["{serveur}"]}}' + + response = requests.post(url, data=payload, headers=headers) + return response.json(),response.status_code + +#************************************************* + + +#************************************************* diff --git a/dockers/apikaz/source/resources/kaz_user.py b/dockers/apikaz/source/resources/kaz_user.py new file mode 100644 index 0000000..9847db9 --- /dev/null +++ b/dockers/apikaz/source/resources/kaz_user.py @@ -0,0 +1,223 @@ +from resources.common_imports import * + +#les variables globales minimum +from resources.config import site_url, webmail_url, mdp_url + + +class Kaz_user(Resource): + + def __init__(self): + 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() + + +#******************************************************************************************** + @jwt_required() + def delete(self): + """ + Utile pour les tests de createUser. Avant le POST de /kaz/create/users. Ça permet de supprimer/maj les comptes. + --- + tags: + - Kaz User + security: + - Bearer: [] + parameters: [] + responses: + 201: + description: Succès + 401: + description: Oops, un soucis quelconque + """ + #verrou pour empêcher de lancer en même temps la même api + try: + prefixe="del_user_lock_" + if glob(f"{tempfile.gettempdir()}/{prefixe}*"): raise ValueError("ERREUR 0 : api déjà en cours d'utilisation !") + lock_file = tempfile.NamedTemporaryFile(prefix=prefixe,delete=True) + + #TODO à remplir à la main + liste_emails=["",""] + email_secours="" + liste_sympa="" + + for email in liste_emails: + res, status_code = self.ldap_user_resource.delete(email) + res, status_code = self.mattermost_user_resource.delete(email) + nom_orga=''.join(random.choice(string.ascii_lowercase) for _ in range(8)) + res, status_code = self.paheko_user_resource.put(email,"nom_orga",nom_orga) + res, status_code = self.paheko_user_resource.put(email,"action_auto","A créer") + res, status_code = self.paheko_user_resource.put(email,"email_secours",email_secours) + res, status_code = self.sympa_user_resource.delete(email,liste_sympa) + res, status_code = self.sympa_user_resource.delete(email_secours,liste_sympa) + msg=f"**POST AUTO** suppression de {email} ok" + self.mattermost_message_resource.post(message=msg) + return "OK", 200 + + except ValueError as e: + msg=f"(classe: {__class__.__name__} : {e}" + self.mattermost_message_resource.post(message=msg) + return str(msg), 401 + +#******************************************************************************************** + + @jwt_required() + def post(self): + """ + Créé un nouveau kaznaute: inscription sur MM / Cloud / email + msg sur MM + email à partir de action="a créer" sur paheko + --- + tags: + - Kaz User + security: + - Bearer: [] + parameters: [] + responses: + 201: + description: Succès, kaznaute créé + 400: + description: Oops, rien à créer + 401: + description: Oops, un soucis quelconque + """ + + try: + + #verrou pour empêcher de lancer en même temps la même api + prefixe="create_user_lock_" + if glob(f"{tempfile.gettempdir()}/{prefixe}*"): raise ValueError("ERREUR 0 : api déjà en cours d'utilisation !") + lock_file = tempfile.NamedTemporaryFile(prefix=prefixe,delete=True) + + #qui sont les kaznautes à créer ? + liste_kaznautes, status_code = self.paheko_users_action_resource.get("A créer") + + if liste_kaznautes=="pas de résultat": return "ERREUR: paheko non joignable",401 + + count=liste_kaznautes['count'] + if count==0: return "aucun nouveau kaznaute à créer",400 + + #au moins un kaznaute à créer + for tab in liste_kaznautes['results']: + email = tab['email'].lower() + + # est-il déjà dans le ldap ? (mail ou alias) + res, status_code = self.ldap_user_resource.get(email) + if status_code != 400: raise ValueError(f"ERREUR 1: {email} déjà existant dans ldap. {res}, on arrête tout") + + #test nom orga + if tab['admin_orga'] == 1: + if tab['nom_orga'] is None: + raise ValueError(f"ERREUR 0 sur paheko: {email} : nom_orga vide, on arrête tout") + if not bool(re.match(r'^[a-z0-9-]+$', tab['nom_orga'])): + raise ValueError(f"ERREUR 0 sur paheko: {email} : nom_orga ({tab['nom_orga']}) incohérent (minuscule/chiffre/-), on arrête tout") + + #test email_secours + email_secours = tab['email_secours'].lower() + if not validate_email(email_secours): raise EmailNotValidError() + + #test quota + quota = tab['quota_disque'] + if not quota.isdigit(): raise ValueError(f"ERREUR 2: quota non numérique : {quota}, on arrête tout") + + #quel type de test ? + #"nom": "ROUSSEAU Mickael", + nom, prenom = tab['nom'].split(maxsplit=1) + + #on génère un password + password,status_code = self.password_create_resource.get() + + #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 + } + res, status_code = self.ldap_user_resource.put(email, **data) + if status_code != 200: raise ValueError(f"ERREUR 3 sur ldap: {email} : {res}, on arrête tout ") + + #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) + + return "fin des inscriptions", 201 + + except EmailNotValidError as e: + msg=f"classe: {__class__.__name__} : ERREUR 13 : email_secours : {email_secours} " + str(e) +", on arrête tout" + self.mattermost_message_resource.post(message=msg) + return msg, 401 + except ValueError as e: + msg=f"(classe: {__class__.__name__} : {e}" + self.mattermost_message_resource.post(message=msg) + return str(msg), 401 + + +#************************************************* + + diff --git a/dockers/apikaz/source/resources/ldap.py b/dockers/apikaz/source/resources/ldap.py new file mode 100644 index 0000000..4f8ab6a --- /dev/null +++ b/dockers/apikaz/source/resources/ldap.py @@ -0,0 +1,341 @@ +from resources.common_imports import * + +#les variables globales minimum +from resources.config import ldap_admin, ldap_pass, ldap_root, ldap_host + +class Ldap_user(Resource): + + def __init__(self): + global ldap_admin, ldap_pass, ldap_root, ldap_host + self.ldap_admin = ldap_admin + self.ldap_pass = ldap_pass + self.ldap_root = ldap_root + self.ldap_host = f"ldap://{ldap_host}" + + def _connect_ldap(self): + ldap_connection = ldap.initialize(self.ldap_host) + ldap_connection.simple_bind_s("cn={},{}".format(self.ldap_admin, self.ldap_root), self.ldap_pass) + return ldap_connection + + @classmethod + def is_valid_field(cls, field): + allowed_fields = ['mailDeSecours', 'mailEnabled', 'nextcloudEnabled', 'mobilizonEnabled', 'agoraEnabled', 'userPassword', 'identifiantKaz', 'mailAlias', 'quota'] + return field in allowed_fields + + @jwt_required() + def get(self, email): + """ + Vérifier si un utilisateur avec cet email existe dans le LDAP soit comme mail principal soit comme alias + --- + tags: + - Ldap + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + responses: + 200: + description: Existe + 400: + description: N'existe pas + 401: + description: oops, email invalide + 402: + description: oops, autre erreur + + """ + try: + + if not validate_email(email): + return "Adresse e-mail non valide", 400 + + ldap_connection = self._connect_ldap() + + #result = ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, "(cn={})".format(email)) + + # Créer une chaîne de filtre pour rechercher dans les champs "cn" et "mailAlias" + filter_str = "(|(cn={})(mailAlias={}))".format(email, email) + result = ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, filter_str) + + ldap_connection.unbind_s() + + if result: + return True, 200 + else: + return False, 400 + + except EmailNotValidError as e: + return str(e), 401 + + except ldap.LDAPError as e: + return str(e), 402 + + +#************************************************* + @jwt_required() + def delete(self, email): + """ + Supprimer un utilisateur du LDAP par son adresse e-mail + --- + tags: + - Ldap + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + responses: + 200: + description: Utilisateur supprimé avec succès + 404: + description: Utilisateur non trouvé dans le LDAP + 400: + description: Erreur lors de la suppression de l'utilisateur + """ + try: + if not validate_email(email): + return "Adresse e-mail non valide", 400 + + ldap_connection = self._connect_ldap() + + # Recherche de l'utilisateur + result = ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, "(cn={})".format(email)) + + if not result: + return False, 404 # Utilisateur non trouvé + + # Récupération du DN de l'utilisateur + dn = result[0][0] + + # Suppression de l'utilisateur + ldap_connection.delete_s(dn) + ldap_connection.unbind_s() + + return True, 200 # Utilisateur supprimé avec succès + + except ldap.NO_SUCH_OBJECT: + return False, 404 # Utilisateur non trouvé + except ldap.LDAPError as e: + return str(e), 400 # Erreur lors de la suppression + except EmailNotValidError as e: + return str(e), 400 + + +#************************************************* + @jwt_required() + def post(self, email): + """ + Ajouter, supprimer ou modifier un champ pour l'utilisateur LDAP + --- + tags: + - Ldap + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + - in: query + name: action + type: string + required: true + enum: ['add', 'delete', 'modify'] + - in: body + name: data + required: true + schema: + type: object + properties: + field: + type: string + enum: ['mailDeSecours', 'mailEnabled', 'nextcloudEnabled', 'mobilizonEnabled', 'agoraEnabled', 'userPassword', 'identifiantKaz', 'mailAlias', 'quota'] + description: Le champ à ajouter, supprimer ou modifier (par exemple, mailDeSecours, mailAlias, etc.) + value: + type: string + description: La valeur à ajouter, supprimer ou modifier pour le champ spécifié + responses: + 200: + description: Opération réussie + 404: + description: Utilisateur non trouvé dans le LDAP + 400: + description: Erreur lors de l'opération + """ + + try: + if not validate_email(email): + return "Adresse e-mail non valide", 400 + + action = request.args.get('action') + field = request.json.get('field') + value = request.json.get('value') + + if not action or not field or not value: + return "Action, champ ou valeur manquant", 400 + + if not self.is_valid_field(field): + return "Champ non autorisé", 400 + + ldap_connection = self._connect_ldap() + result = ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, "(cn={})".format(email)) + + if not result: + return False, 404 + + dn = result[0][0] + + if field == 'userPassword' and (action == 'add' or action == 'modify'): + password_chiffre = sha512_crypt.hash(value) + value = "{{CRYPT}}{}".format(password_chiffre) + + if action == 'add': + mod_attrs = [(ldap.MOD_ADD, field, value.encode('utf-8'))] + + elif action == 'delete': + mod_attrs = [(ldap.MOD_DELETE, field, value.encode('utf-8'))] + + elif action == 'modify': + + if field == 'quota': + mail_quota_value = value + 'G' + nextcloud_quota_value = value + " GB" + mod_attrs = [ + (ldap.MOD_REPLACE, 'quota', value.encode('utf-8')), + (ldap.MOD_REPLACE, 'mailQuota', mail_quota_value.encode('utf-8')), + (ldap.MOD_REPLACE, 'nextcloudQuota', nextcloud_quota_value.encode('utf-8')) + ] + else: + mod_attrs = [(ldap.MOD_REPLACE, field, value.encode('utf-8'))] + + else: + return "Action non valide", 400 + + ldap_connection.modify_s(dn, mod_attrs) + ldap_connection.unbind_s() + + return True, 200 + + except ldap.NO_SUCH_OBJECT: + return False, 404 + except ldap.LDAPError as e: + return str(e), 400 + except EmailNotValidError as e: + return str(e), 400 + +#************************************************* + @jwt_required() + def put(self, email, **kwargs): + """ + 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 ? + --- + tags: + - Ldap + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + - in: body + name: data + required: true + schema: + type: object + properties: + prenom: + type: string + description: Prénom de l'utilisateur + nom: + type: string + description: Nom de l'utilisateur + password: + type: string + description: Mot de passe de l'utilisateur + email_secours: + type: string + description: Adresse e-mail de secours + quota: + type: string + description: Quota de l'utilisateur + responses: + 200: + description: Utilisateur ajouté avec succès + 400: + description: Erreur lors de l'ajout de l'utilisateur + 406: + description: Erreur utilisateur déjà existant + + """ + try: + + if kwargs: # appel depuis une autre api + email_secours = kwargs.get('email_secours') + prenom = kwargs.get('prenom') + nom = kwargs.get('nom') + password = kwargs.get('password') + quota = kwargs.get('quota') + + else: # appel depuis swagger + email_secours = request.json.get('email_secours') + nom = request.json.get('nom') + prenom = request.json.get('prenom') + password = request.json.get('password') + quota = request.json.get('quota') + + password_chiffre = sha512_crypt.hash(password) + + if not validate_email(email) or not validate_email(email_secours): + return "Adresse e-mail ou secours non valide", 400 + + #le user existe t-il déjà ? + ldap_connection = self._connect_ldap() + result = ldap_connection.search_s("ou=users,{}".format(self.ldap_root), ldap.SCOPE_SUBTREE, "(cn={})".format(email)) + + if result: + return "User déjà existant", 406 + + # Construire le DN + dn = f"cn={email},ou=users,{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')) + ] + + ldap_connection.add_s(dn, mod_attrs) + ldap_connection.unbind_s() + + return "Utilisateur créé dand le ldap", 200 + + except ldap.LDAPError as e: + return str(e), 400 + except EmailNotValidError as e: + return str(e), 400 + +#************************************************* + + + diff --git a/dockers/apikaz/source/resources/mattermost.py b/dockers/apikaz/source/resources/mattermost.py new file mode 100644 index 0000000..b775b4d --- /dev/null +++ b/dockers/apikaz/source/resources/mattermost.py @@ -0,0 +1,394 @@ +from resources.common_imports import * + +#les variables globales minimum +from resources.config import mattermost_user, mattermost_pass, mattermost_url + +# on utilise mmctl et pas l'apiv4 de MM +# pourquoi ? passe que mmctl déjà utilisé dans les scripts kaz. + +#************************************************* + +def Mattermost_authenticate(): + # Authentification sur MM + global mattermost_url, mattermost_user, mattermost_pass + cmd = f"/mm/mmctl auth login {mattermost_url} --name local-server --username {mattermost_user} --password {mattermost_pass}" + subprocess.run(cmd, shell=True, stderr=subprocess.STDOUT, check=True) + +#************************************************* + +class Mattermost_message(Resource): + + @jwt_required() + def post(self,message,equipe="kaz",canal="creation-comptes"): + """ + Envoyer un message dans une Equipe/Canal de MM + --- + tags: + - Mattermost + security: + - Bearer: [] + parameters: + - in: path + name: equipe + type: string + required: true + - in: path + name: canal + type: string + required: true + - in: path + name: message + type: string + required: true + + responses: + 200: + description: Affiche un message dans un canal d'une équipe + 500: + description: oops + """ + Mattermost_authenticate() + + try: + cmd="/mm/mmctl post create "+equipe+":"+canal+" --message "+ "\"" + message + "\"" + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + return "Message envoyé", 200 + except subprocess.CalledProcessError: + return "Message non envoyé", 500 + + +#************************************************* + +class Mattermost_user(Resource): + + def __init__(self): + Mattermost_authenticate() + +#************************************************* + @jwt_required() + def get(self,user): + """ + Le user existe t-il sur MM ? + --- + tags: + - Mattermost User + security: + - Bearer: [] + parameters: + - in: path + name: user + type: string + required: true + description: possible d'entrer un username, un email + responses: + 200: + description: Existe + 404: + description: N'existe pas + """ + + try: + cmd = f"/mm/mmctl user search {user} --json" + user_list_output = subprocess.check_output(cmd, shell=True) + return 200 # Le nom d'utilisateur existe + except subprocess.CalledProcessError: + return 404 # Le nom d'utilisateur n'existe pas + +#************************************************* + @jwt_required() + def post(self,user,email,password): + """ + Créer un utilisateur sur MM + --- + tags: + - Mattermost User + security: + - Bearer: [] + parameters: + - in: path + name: user + type: string + required: true + - in: path + name: email + type: string + required: true + - in: path + name: password + type: string + required: true + responses: + 200: + description: Utilisateur créé + 400: + description: oops, Utilisateur non créé + """ + + # Création de l'utilisateur + try: + cmd = f"/mm/mmctl user create --email {email} --username {user} --password {password}" + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + return output.decode("utf-8"), 200 + + except subprocess.CalledProcessError as e: + return e.output.decode("utf-8"), 400 + +#************************************************* + + @jwt_required() + def delete(self,email): + """ + Supprimer un utilisateur sur MM + --- + tags: + - Mattermost User + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + responses: + 200: + description: Utilisateur supprimé + 400: + description: oops, Utilisateur non supprimé + """ + + try: + cmd = f"/mm/mmctl user delete {email} --confirm" + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + return output.decode("utf-8"), 200 + + except subprocess.CalledProcessError as e: + return e.output.decode("utf-8"), 400 + +#************************************************* + + @jwt_required() + def put(self,email,new_password): + """ + Changer un password pour un utilisateur de MM + --- + tags: + - Mattermost User + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + - in: path + name: new_password + type: string + required: true + responses: + 200: + description: Mot de passe de l'Utilisateur changé + 400: + description: oops, Mot de passe de l'Utilisateur inchangé + """ + try: + cmd = f"/mm/mmctl user change-password {email} --password {new_password}" + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + return output.decode("utf-8"), 200 + + except subprocess.CalledProcessError as e: + return e.output.decode("utf-8"), 400 + +#************************************************* + + +#************************************************* + +class Mattermost_user_team(Resource): + + @jwt_required() + def post(self,email,equipe): + """ + Affecte un utilisateur à une équipe MM + --- + tags: + - Mattermost Team + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + - in: path + name: equipe + type: string + required: true + responses: + 200: + description: l'utilisateur a bien été affecté à l'équipe + 400: + description: oops, Utilisateur non affecté + """ + Mattermost_authenticate() + + try: + cmd = f"/mm/mmctl team users add {equipe} {email}" + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + return output.decode("utf-8"), 200 + + except subprocess.CalledProcessError as e: + return e.output.decode("utf-8"), 400 + + +#************************************************* + +class Mattermost_user_channel(Resource): + @jwt_required() + def post(self,email,equipe,canal): + """ + Affecte un utilisateur à un canal MM + --- + tags: + - Mattermost + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + - in: path + name: equipe + type: string + required: true + - in: path + name: canal + type: string + required: true + responses: + 200: + description: l'utilisateur a bien été affecté au canal + 400: + description: oops, Utilisateur non affecté + """ + Mattermost_authenticate() + + try: + cmd = f'/mm/mmctl channel users add {equipe}:{canal} {email}' + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + return output.decode("utf-8"), 200 + + except subprocess.CalledProcessError as e: + return e.output.decode("utf-8"), 400 + + +#************************************************* + +class Mattermost_team(Resource): + + def __init__(self): + Mattermost_authenticate() + +#************************************************* + + @jwt_required() + def get(self): + """ + Lister les équipes sur MM + --- + tags: + - Mattermost Team + security: + - Bearer: [] + parameters: [] + responses: + 200: + description: liste des équipes + 400: + description: oops, Equipe non supprimée + """ + Mattermost_authenticate() + + try: + cmd = f"/mm/mmctl team list --disable-pager" + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + data_list = output.decode("utf-8").strip().split('\n') + data_list.pop() + return data_list, 200 + #return jsonify(data_list),200 + + except subprocess.CalledProcessError as e: + return e.output.decode("utf-8"), 400 + +#************************************************* + @jwt_required() + def post(self,equipe,email): + """ + Créer une équipe sur MM et affecter un admin si email est renseigné (set admin marche pô) + --- + tags: + - Mattermost Team + security: + - Bearer: [] + parameters: + - in: path + name: equipe + type: string + required: true + - in: path + name: email + type: string + required: true + description: admin de l'équipe + responses: + 200: + description: Equipe créée + 400: + description: oops, Equipe non créée + """ + + try: + #DANGER: l'option --email ne rend pas le user admin de l'équipe comme c'est indiqué dans la doc :( + cmd = f"/mm/mmctl team create --name {equipe} --display-name {equipe} --private --email {email}" + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + + #Workaround: on récup l'id du user et de l'équipe pour affecter le rôle "scheme_admin": true, "scheme_user": true avec l'api MM classique. + #TODO: + + return output.decode("utf-8"), 200 + + except subprocess.CalledProcessError as e: + return e.output.decode("utf-8"), 400 + +#************************************************* + @jwt_required() + def delete(self,equipe): + """ + Supprimer une équipe sur MM + --- + tags: + - Mattermost Team + security: + - Bearer: [] + parameters: + - in: path + name: equipe + type: string + required: true + responses: + 200: + description: Equipe supprimée + 400: + description: oops, Equipe non supprimée + """ + Mattermost_authenticate() + + try: + cmd = f"/mm/mmctl team delete {equipe} --confirm" + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + return output.decode("utf-8"), 200 + + except subprocess.CalledProcessError as e: + return e.output.decode("utf-8"), 400 + +#************************************************* diff --git a/dockers/apikaz/source/resources/paheko.py b/dockers/apikaz/source/resources/paheko.py new file mode 100644 index 0000000..0e615eb --- /dev/null +++ b/dockers/apikaz/source/resources/paheko.py @@ -0,0 +1,256 @@ +from resources.common_imports import * + +#les variables globales minimum +from resources.config import paheko_ident, paheko_pass, paheko_url + + +class Paheko_categories(Resource): + + @jwt_required() + def get(self): + """ + Récupérer les catégories Paheko avec le compteur associé + --- + tags: + - Paheko + security: + - Bearer: [] + parameters: [] + responses: + 200: + description: Liste des catégories Paheko + 404: + description: oops + """ + 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 jsonify(data) + else: + return jsonify({'error': 'La requête a échoué'}), response.status_code + + +#************************************************* + +class Paheko_users(Resource): + + @jwt_required() + def get(self,categorie): + """ + Afficher les membres d'une catégorie Paheko + --- + tags: + - Paheko + security: + - Bearer: [] + parameters: + - in: path + name: categorie + type: string + required: true + responses: + 200: + description: Liste des membres une catégorie Paheko + 404: + description: oops + """ + + 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 jsonify(data) + else: + return jsonify({'error': 'La requête a échoué'}), response.status_code + + +#************************************************* + +class Paheko_user(Resource): + + def __init__(self): + global paheko_ident, paheko_pass, paheko_url + self.paheko_ident = paheko_ident + self.paheko_pass = paheko_pass + self.paheko_url = paheko_url + self.auth = (self.paheko_ident, self.paheko_pass) + + @jwt_required() + def get(self,ident): + """ + Afficher un membre de Paheko par son email kaz ou son numéro ou le non court de l'orga + --- + tags: + - Paheko + security: + - Bearer: [] + parameters: + - in: path + name: ident + type: string + required: true + description: possible d'entrer un numéro, un email, le nom court de l'orga + responses: + 200: + description: Existe et affiche + 404: + description: N'existe pas + """ + + 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 jsonify(data["results"][0]) + elif data["count"] == 0: + return "pas de résultat", 400 + else: + return "Plusieurs utilisateurs correspondent ?!", 400 + else: + #return jsonify({'error': 'La requête a échoué'}), response.status_code + return "pas de résultat", response.status_code + +#************************************************* + + @jwt_required() + def put(self,ident,field,new_value): + """ + Modifie la valeur d'un champ d'un membre paheko (ident= numéro paheko ou email kaz) + --- + tags: + - Paheko + security: + - Bearer: [] + parameters: + - in: path + name: ident + type: string + required: true + description: possible d'entrer le numéro paheko, un email kaz + - in: path + name: field + type: string + required: true + description: un champ de la table users de la base paheko + - in: path + name: new_value + type: string + required: true + description: la nouvelle valeur à remplacer + + responses: + 200: + description: Modification effectuée avec succès + 400: + description: Oops, ident non trouvé ou incohérent + 404: + description: Oops, modification du champ KO + """ + + #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: + return "email non trouvé", 400 + elif data['count'] > 1: + return "trop de résultat", 400 + else: + #OK + ident = data['results'][0]['id'] + else: + return "pas de résultat", response.status_code + elif not ident.isdigit(): + return "Identifiant utilisateur invalide", response.status_code + + 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(),response.status_code + +#************************************************* + + +#************************************************* + +class Paheko_users_action(Resource): + + def __init__(self): + global paheko_ident, paheko_pass, paheko_url + self.paheko_ident = paheko_ident + self.paheko_pass = paheko_pass + self.paheko_url = paheko_url + + @jwt_required() + def get(self, action): + """ + retourne tous les membres de paheko avec une action à mener (création du compte kaz / modification...) + --- + tags: + - Paheko + security: + - Bearer: [] + parameters: + - in: path + name: action + type: string + required: true + enum: ['A créer','A modifier','En attente','Aucune'] + responses: + 200: + description: liste des nouveaux kaznautes à créer + 404: + description: aucun nouveau kaznaute à créer + """ + auth = (self.paheko_ident, self.paheko_pass) + + api_url = self.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(),200 + else: + return "pas de résultat", response.status_code + + diff --git a/dockers/apikaz/source/resources/password.py b/dockers/apikaz/source/resources/password.py new file mode 100644 index 0000000..dc6f116 --- /dev/null +++ b/dockers/apikaz/source/resources/password.py @@ -0,0 +1,34 @@ +from resources.common_imports import * + +#les variables globales minimum +#from resources.config import + + +class Password_create(Resource): + @jwt_required() + def get(self): + """ + créer un password qui colle avec les appli kaz + --- + tags: + - Password + security: + - Bearer: [] + parameters: [] + responses: + 200: + description: le password + 404: + description: oops + """ + global new_password + + cmd="apg -n 1 -m 10 -M NCL -d" + try: + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + new_password="_"+output.decode("utf-8")+"_" + return new_password,200 + + except subprocess.CalledProcessError as e: + return e.output.decode("utf-8"), 400 + diff --git a/dockers/apikaz/source/resources/quota.py b/dockers/apikaz/source/resources/quota.py new file mode 100644 index 0000000..5b97af1 --- /dev/null +++ b/dockers/apikaz/source/resources/quota.py @@ -0,0 +1,64 @@ +from resources.common_imports import * + +#les variables globales minimum +from resources.config import serveur_imap, mot_de_passe_mail + + +class Quota(Resource): + +#https://doc.dovecot.org/configuration_manual/authentication/master_users/ + +#https://blog.debugo.fr/serveur-messagerie-dovecot/ +# sur kazkouil.fr, j'ai modifié /etc/dovecot/conf.d/20-lmtp.conf +#mail_plugins = $mail_plugins sieve quota + + @jwt_required() + def get(self, email): + """ + Récupérer la place prise par une BAL (EN COURS) + --- + tags: + - Quota + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + responses: + 200: + description: Succès, taille d'une BAL' + 400: + description: Oops, échec dans l'affichage de la taille d'une BAL + """ + global serveur_imap, mot_de_passe_mail + + try: + if validate_email(email): + # Connexion au serveur IMAP + mail = imaplib.IMAP4_SSL(serveur_imap) + mail.login(email, mot_de_passe_mail) + + #res, data = mail.select("INBOX") + #return data[0].decode("utf-8"), 200 + + # Requête pour obtenir le quota de la boîte aux lettres + # res, data = mail.getquota("INBOX") + # return str(data[0]).split() + + res, data = mail.getquotaroot("INBOX") + return str(data[1]).split()[3]+" KB" + + # Fermeture de la connexion + mail.close() + mail.logout() + + else: + return "Email non valide", 400 + + except imaplib.IMAP4.error as e: + return str(e), 400 # Retourne le message d'erreur et un code d'erreur 400 + except EmailNotValidError as e: + return str(e), 400 # Retourne le message d'erreur et un code d'erreur 400 + diff --git a/dockers/apikaz/source/resources/sympa.py b/dockers/apikaz/source/resources/sympa.py new file mode 100644 index 0000000..318b91c --- /dev/null +++ b/dockers/apikaz/source/resources/sympa.py @@ -0,0 +1,81 @@ + +from resources.common_imports import * + +#les variables globales minimum +from resources.config import sympa_ident, sympa_pass, sympa_url, sympa_liste_info, MAIL_USERNAME + + +class Sympa_user(Resource): + def __init__(self): + global sympa_ident, sympa_pass, sympa_url,MAIL_USERNAME + self.sympa_ident = sympa_ident + self.sympa_pass = sympa_pass + self.sympa_url = sympa_url + + def _execute_sympa_command(self, email, liste, service): + try: + if validate_email(email) and validate_email(liste): + cmd = f'export PERL5LIB=/usr/src/app/:$PERL5LIB && /usr/src/app/Sympa/sympa_soap_client.pl --soap_url={self.sympa_url}/sympasoap --trusted_application={self.sympa_ident} --trusted_application_password={self.sympa_pass} --proxy_vars=USER_EMAIL={MAIL_USERNAME} --service={service} --service_parameters="{liste},{email}" && echo $?' + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + return output.decode("utf-8"), 200 # Retourne la sortie et un code de succès + except EmailNotValidError as e: + return str(e), 400 # Retourne le message d'erreur et un code d'erreur 400 + except subprocess.CalledProcessError as e: + return e.output.decode("utf-8"), 400 # Retourne la sortie de la commande et un code d'erreur 400 + + @jwt_required() + def post(self, email, liste): + """ + Ajouter un email dans une liste sympa + --- + tags: + - Sympa + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + - in: path + name: liste + type: string + required: true + responses: + 200: + description: Succès, email ajouté dans la liste + 400: + description: Oops, email non ajouté dans la liste + """ + output, status_code = self._execute_sympa_command(email, liste, 'add') + return output, status_code + + @jwt_required() + def delete(self, email, liste): + """ + Supprimer un email dans une liste sympa + --- + tags: + - Sympa + security: + - Bearer: [] + parameters: + - in: path + name: email + type: string + required: true + - in: path + name: liste + type: string + required: true + responses: + 200: + description: Succès, email supprimé dans la liste + 400: + description: Oops, email non supprimé dans la liste + """ + output, status_code = self._execute_sympa_command(email, liste, 'del') + return output, status_code + + + diff --git a/dockers/apikaz/source/resources/test.py b/dockers/apikaz/source/resources/test.py new file mode 100644 index 0000000..62864c2 --- /dev/null +++ b/dockers/apikaz/source/resources/test.py @@ -0,0 +1,111 @@ +from resources.common_imports import * + +#les variables globales minimum +from resources.config import * + + +class Test(Resource): + + def __init__(self): + toto="toto" + #self.mattermost_team_resource=Mattermost_team() + #global mattermost_url, sympa_url, webmail_url, mdp_url, site_url, nc_url + +#******************************************************************************************** + @jwt_required() + def get(self): + """ + Pour tester des conneries: # test lançement de cmde ssh sur des serveurs distants: + --- + tags: + - Test + security: + - Bearer: [] + parameters: [] + responses: + 201: + description: OK + 401: + description: KO + """ +#******************************************************************************************** + + +# test lançcement de cmde ssh sur des serveurs distants: +# il faut au préalable que la clé publique de root du conteneur apikaz soit dans authorized key du user fabricer de la machine 163.172.94.54 +# clé à créer dans le Dockerfile +# risque sécu ? + + + cmd="ssh -p 2201 fabricer@163.172.94.54 mkdir -p /tmp/toto" + try: + output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) + return "ok",200 + + except subprocess.CalledProcessError as e: + return e.output.decode("utf-8"), 400 + +#******************************************************************************************** +# #***** test suppression de toutes les équipes de MM sauf KAZ +# res,status=self.mattermost_team_resource=Mattermost_team().get() +# for equipe in res: +# if equipe!="kaz": res,status=self.mattermost_team_resource=Mattermost_team().delete(equipe) +# return "fin" + +#******************************************************************************************** + +#**** test messagerie + +# NOM="toto" +# EMAIL_SOUHAITE='f@kaz.bzh' +# PASSWORD="toto" +# QUOTA="1" +# ADMIN_ORGA="0" +# +# context = { +# 'ADMIN_ORGA': ADMIN_ORGA, +# 'NOM': NOM, +# 'EMAIL_SOUHAITE': EMAIL_SOUHAITE, +# 'PASSWORD': PASSWORD, +# 'QUOTA': QUOTA, +# '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_SOUHAITE]) +# msg.html = render_template('email_inscription.html', **context) +# +# # Parsez le contenu HTML avec BeautifulSoup +# soup = BeautifulSoup(msg.html, 'html.parser') +# msg.body = soup.get_text() +# +# mail.send(msg) +# return "Message envoyé!" + +#******************************************************************************************** +# #**** test ms erreur +# email_secours="toto" +# +# msg=f"classe: {__class__.__name__} : ERREUR 8 : email_secours : {email_secours} " +", on arrête tout" +# #return __class__.__name__ +# return msg + +#******************************************************************************************** +#**** test vérou +# prefixe="toto_" +# if glob(f"{tempfile.gettempdir()}/{prefixe}*"): +# return "ERREUR : api déjà en cours d'utilisation !", 400 +# else: +# lock_file = tempfile.NamedTemporaryFile(prefix=prefixe,delete=True) +# +# sleep(20) +# return str(lock_file), 201 +