# -*- coding: UTF-8 -*-
###########################################################################
# Eole NG - 2007
# Copyright Pole de Competence Eole  (Ministere Education - Academie Dijon)
# Licence CeCill  cf /root/LicenceEole.txt
# eole@ac-dijon.fr
#
# users_rpc.py
#
# fonctions xmlrpc pour la gestion des utilisateurs et permissions
#
###########################################################################
"""module de gestion des utilisateurs de l'application
"""
from zephir.backend import config
from zephir.backend.config import u, log
from zephir.backend.db_utils import *
from zephir.backend.lib_backend import serveur_pool
from zephir.backend.schedule import TaskScheduler
from zephir.backend.dictpool import init_dictpool
from zephir.backend.xmlrpceole import XMLRPCEole as XMLRPC
from zephir.utils.ldap_user import add_user
from zephir.utils.convert import to_bytes, to_string

import sys,base64,ldap,os
import traceback
import apt_pkg

class RPCUsers(XMLRPC):
    """serveur XMLRPC zephir pour la gestion des utilisateurs de l'application
    """

    def __init__(self):
        self.dbpool = db_connect()
        self.dbpool.noisy = 0
        # pool de représentation des serveurs et dictionnaires Creole
        self.s_pool = serveur_pool()
        self.dictpool = init_dictpool(self.s_pool)
        # planificateur de tâche (non fonctionnel)
        # self.scheduler = TaskScheduler(self.s_pool)
        self.maj_checker = None
        XMLRPC.__init__(self)
        self.reload_perms=0

    def xmlrpc_get_stats(self, cred_user):
        """renvoie des statistiques générales sur les serveurs"""
        return 1, u(self.s_pool.get_stats())

    def xmlrpc_get_rights(self,cred_user):
        """liste des groupes de droits"""
        # on précharge les groupes de droits
        query = """select * from groupes_droits"""
        return self.dbpool.runQuery(query).addCallbacks(self._load_rights,db_client_failed)

    def _load_rights(self,data):
        """formattage des groupes de droits
        """
        # on crée en mémoire un dictionnaire des groupes de droits
        droits=[]
        for groupe in data:
            # dictionnaire du type : {id:[droits,libelle]}
            droits.append([groupe[0],groupe[1],eval(groupe[2])])
        return 1, u(droits)

    def xmlrpc_get_permissions(self,cred_user,username):
        """liste des permissions de l'application pour un utilisateur précis"""
        # on récupère la liste des groupes de permissions pour l'utilisateur
        query = """select login, droits from users where login = %s"""
        return self.dbpool.runQuery(query, to_string((username,))).addCallbacks(self._get_permissions,db_client_failed)

    def _get_permissions(self,data):
        try:
            # si l'utilisateur existe, on récupère ses groupes de droits
            username = data[0][0]
            groupes = eval(data[0][1])
        except:
            # aucun groupe autorisé pour cet utilisateur
            return 1, []

        return 1, u(groupes)

    def xmlrpc_save_permissions(self,cred_user,username,user_auths):
        """sauvegarde les autorisations de l'utilisateur"""

        # on regarde si l'utilisateur existe dans la base de données
        query = """select login,nom,prenom,mail from users where login = %s"""
        return self.dbpool.runQuery(query, to_string((username,))).addCallbacks(self._save_permissions,db_client_failed,callbackArgs=[username,user_auths])

    def _save_permissions(self,data,username,user_auths):
        # on tente de récupèrer certaines valeurs dans l'annuaire (mail, nom, prenom)
        if data!=[]:
            nom=data[0][1]
            prenom=data[0][2]
            mail=data[0][3]
        else:
            nom=""
            prenom=""
            mail=""
        try:
            l = ldap.initialize("ldap://{}".format(config.ADRESSE_LDAP))
            if config.LDAP_TLS == "oui":
                l.start_tls_s()
            # on récupère le dn complet de l'utilisateur
            result=l.search_s(config.BASE_LDAP, ldap.SCOPE_SUBTREE, "(uid="+username+")")
            res_ldap = result[0][1]
            l.unbind()
            if nom == "":
                try:
                    nom=res_ldap['sn'][0]
                except KeyError:
                    pass
            if prenom == "":
                try:
                    prenom=res_ldap['givenName'][0]
                except KeyError:
                    pass
            if mail == "":
                try:
                    mail=res_ldap['mail'][0]
                except KeyError:
                    pass
        except:
            pass
        if data != []:
            # si utilisateur existant, on le met à jour
            query = """update users set droits=%s,nom=%s,prenom=%s,mail=%s where login=%s"""
            params = to_string((user_auths, nom, prenom, mail, username))
        else:
            # sinon on le crée avec des valeurs par défaut
            query = """insert into users (login,mail_actif,sms_actif,droits,nom,prenom,mail)  \
                    values (%s,%s,%s,%s,%s,%s,%s)"""
            params = to_string((username, 0, 0, user_auths, nom, prenom, mail))

        return self.dbpool.runOperation(query, params).addCallbacks(lambda x : [1,'ok'],db_client_failed)

    def xmlrpc_get_restrictions(self, cred_user, login, type_res=None):
        """ajoute des restrictions sur les serveurs accessibles à un utilisateur
        """
        try:
            if isinstance(type_res, bytes):
                type_res = type_res
            res = self.s_pool.get_restrictions(login, type_res)
        except KeyError:
            return 0, u("utilisateur ou type de contrainte non valides")
        return 1, u(res)

    def xmlrpc_add_restriction(self, cred_user, login, type_res, id_res):
        """ajoute des restrictions sur les serveurs accessibles à un utilisateur
        """
        # on regarde si l'utilisateur existe
        query = """select login,nom,prenom,mail from users where login = %s"""
        return self.dbpool.runQuery(query, to_string((login,))).addCallbacks(self._add_restriction,db_client_failed,callbackArgs=[login, type_res, id_res])

    def _add_restriction(self,data,login,type_res,id_res):
        # on tente de récupèrer certaines valeurs dans l'annuaire (mail, nom, prenom)
        if data!=[]:
            nom=data[0][1]
            prenom=data[0][2]
            mail=data[0][3]
        else:
            nom=""
            prenom=""
            mail=""
        try:
            l = ldap.initialize("ldap://{}".format(config.ADRESSE_LDAP))
            if config.LDAP_TLS == "oui":
                l.start_tls_s()
            # on récupère le dn complet de l'utilisateur
            result=l.search_s(config.BASE_LDAP, ldap.SCOPE_SUBTREE, "(uid="+login+")")
            res_ldap = result[0][1]
            l.unbind()
            if nom == "":
                try:
                    nom=res_ldap['sn'][0]
                except KeyError:
                    pass
            if prenom == "":
                try:
                    prenom=res_ldap['givenName'][0]
                except KeyError:
                    pass
            if mail == "":
                try:
                    mail=res_ldap['mail'][0]
                except KeyError:
                    pass
        except:
            pass
        if data == []:
            user_auths = []
            # si l'utilisateur n'existe pas encore, on le crée avec des valeurs par défaut
            query = """insert into users (login,mail_actif,sms_actif,droits,nom,prenom,mail) \
            values (%s,%s,%s,%s,%s,%s,%s)"""
            params = to_string((login, 0, 0, user_auths, nom, prenom, mail))
            return self.dbpool.runOperation(query, params).addCallbacks(self._add_restriction2,db_client_failed,callbackArgs=[login, type_res, id_res])
        else:
            return self._add_restriction2(None, login, type_res, id_res)

    def _add_restriction2(self,retour,login,type_res,id_res):
        # ajout réel de la restriction demandée
        if self.s_pool.add_restriction(login, type_res, id_res):
            return 1, "ok"
        else:
            return 0, u("erreur lors de l'ajout de la contrainte")

    def xmlrpc_del_restriction(self, cred_user, login, type_res, id_res):
        """ajoute des restrictions sur les serveurs accessibles à un utilisateur
        """
        if self.s_pool.del_restriction(login, type_res, id_res):
            return 1, "ok"
        else:
            return 0, u("erreur, contrainte non retrouvée")

    def xmlrpc_get_allowed_servers(self, cred_user, login):
        """renvoie la liste des serveurs accessibles pour un utilisateur (login)
        """
        try:
            return 1, u(self.s_pool.get_allowed_servers(login))
        except Exception as e:
            return 0, u("Erreur lors de la recherche des serveurs accessibles pour %s" % login)

    def xmlrpc_user_group(self,cred_user,username,groupes):
        """met à jour la liste des groupes surveillés par l'utilisateur"""
        query = """update users set groupes = %s where login = %s"""
        params = to_string((str(groupes),username))
        return self.dbpool.runOperation(query, params).addCallbacks(lambda x : [1,'ok'],db_client_failed)

    def xmlrpc_get_user(self,cred_user,username):
        """renvoie les informations d'un utilisateur
        """
        query = """select login,mail,nom,prenom,sms,mail_actif,sms_actif,droits,groupes,cle from users where login = %s"""
        return self.dbpool.runQuery(query, to_string((username,))).addCallbacks(self._get_user,db_client_failed)

    def xmlrpc_list_users(self,cred_user):
        """retourne la liste des utilisateurs"""
        query = """select login from users order by login asc"""
        return self.dbpool.runQuery(query).addCallbacks(self._list_users,db_client_failed)

    def _list_users(self,data):
        """formate la sortie de la base de données"""
        logins=[]
        for user in data:
            logins.append(user[0])
        return 1,u(logins)

    def xmlrpc_del_user(self,cred_user,login):
        """suppression d'un utilisateur"""
        if login:
            # on interdit la suppression si des variantes appartiennent à l'utilisateur
            query = """select libelle from variantes where owner=%s"""
            return self.dbpool.runQuery(query, to_string((login,))).addCallbacks(self._del_user,db_client_failed,callbackArgs=[cred_user, login])
        else:
            return 0,u("""donnez un identifiant""")

    def _del_user(self, data, cred_user, login):
        """suppression de l'utilisateur dans la base de données
        """
        query = """delete from restrictions where login=%s"""
        self.dbpool.runOperation(query, (login,))
        query = """delete from serveur_auth where login=%s"""
        self.dbpool.runOperation(query, (login,))
        query = """delete from users where login=%s"""
        return self.dbpool.runOperation(query, (login,)).addCallbacks(self._update_variantes, db_client_failed, callbackArgs=[data, cred_user, login])

    def _update_variantes(self, res_query, var_data, cred_user, login):
        """réassignation des variantes si besoin
        """
        if len(var_data) > 0:
            # des variantes appartiennent à cet utilisateur, on les attribue à l'utilisateur courant
            log.msg("""Utilisateur %s supprimé, réattribution des variantes suivantes à %s : %s""" % (login, cred_user, ", ".join([lib[0] for lib in var_data])))
            query = """update variantes set owner=%s where owner=%s"""
            return self.dbpool.runOperation(query, to_string((cred_user, login))).addCallbacks(lambda x : [1,'OK'], db_client_failed)
        return 1, "OK"

    def xmlrpc_edit_user(self,cred_user,username,nom,prenom,mail,sms,mail_actif,sms_actif):
        """édite les informations d'un utilisateur
        """
        query = """select * from users where login=%s"""
        return self.dbpool.runQuery(query, to_string((username,))).addCallbacks(self._edit_user,db_client_failed,callbackArgs=[username,nom,prenom,mail,sms,mail_actif,sms_actif])

    def _edit_user(self,data,username,nom,prenom,mail,sms,mail_actif,sms_actif):
        """
        """
        if data == []:
            # utilisateur inexistant
            query = """insert into users (login, nom, prenom, mail, sms, mail_actif, sms_actif, droits, \
                    groupes, cle) values (%s,%s,%s,%s,%s,%s,%s,'','','')"""
            params = to_string((username, nom, prenom, mail, sms, int(mail_actif), int(sms_actif)))
        else:
            query = """update users set nom=%s, prenom=%s, mail=%s, sms=%s, mail_actif=%s, sms_actif=%s where login=%s"""
            params = to_string((nom, prenom, mail, sms, int(mail_actif), int(sms_actif), username))

        return self.dbpool.runOperation(query, params).addCallbacks(lambda x : [1,'ok'],db_client_failed)

    def _get_user(self,data):
        """formate la sortie de la base de données"""
        user=[]
        if data != []:
            for field in data[0]:
                if field in [None, 'None']:
                    user.append("")
                else:
                    user.append(field)

            if user[7] == "":
                user[7] = "[]"
            if user[8] == "":
                user[8] = "[]"
            if user[9] != "":
                user[9] = 1
            else:
                user[9] = 0

            return 1, u([user[0],user[1],user[2],user[3],user[4],int(user[5]),int(user[6]),eval(user[7]),eval(user[8]),user[9]])
        else:
            return 1, []

    def xmlrpc_save_key(self,cred_user,cle):
        """sauvegarde la cle ssh de l'utilisateur"""
        query = """update users set cle=%s where login=%s"""
        params = to_string((cle, cred_user))
        return self.dbpool.runOperation(query, params).addCallbacks(lambda x : [1,'ok'], db_client_failed)

    def xmlrpc_update_client(self,cred_user,name,content=""):
        """fonction de mise à jour du client disponible sur zephir"""
        # on supprime l'ancienne version du fichier.
        try:
            rpm_dir = os.path.abspath(config.PATH_ZEPHIR)+"/sites/client/"
            if not os.path.isdir(rpm_dir):
                os.makedirs(rpm_dir)
            b_name = os.path.basename(name)
            rpms = os.listdir(rpm_dir)
            for rpm in rpms:
                # on enlève les rpm avec même version (majeure+mineure)
                if rpm.startswith(b_name[:b_name.rindex('-')]) and rpm.endswith(os.path.splitext(b_name)[1]):
                    os.unlink(rpm_dir+rpm)
            # on écrit le nouveau paquet
            # si pas de contenu, on essaye de télécharger la version officielle du dépôt (EoleNG seulement)
            if content == "":
                import urllib.request, urllib.parse, urllib.error
                arch_url = ''
                version = b_name.replace('zephir-client', '')[1:4]
                if version not in ('2.0', '2.1'):
                    arch_url = 'all/'
                f_name, message = urllib.request.urlretrieve('http://' + config.CLIENT_UPDATE_HOST + \
                        config.CLIENT_NG_URL % (version, arch_url) + b_name, rpm_dir + b_name)
            else:
                content = base64.decodebytes(to_bytes(content))
                fic_rpm = open(rpm_dir + b_name,'w')
                fic_rpm.write(content)
                fic_rpm.close()
            return 1, "OK"
        except Exception as e:
            traceback.print_exc()
            return 0, u("Erreur de mise a jour du client")

    def xmlrpc_remove_client(self,cred_user,name):
        """suppression d'un client disponible sur zephir"""
        try:
            rpm_dir = os.path.abspath(config.PATH_ZEPHIR)+"/sites/client/"
            rpm = os.path.join(rpm_dir,name)
            if not os.path.isfile(rpm):
                return 0, u("Fichier non trouvé %s" % name)
            os.unlink(rpm)
            return 1, "OK"
        except Exception as e:
            return 0, u("Erreur de suppression du fichier")

    def xmlrpc_maj_client(self,cred_user,version):
        """fonction de vérification de la version du client"""
        # hack pour éviter les problèmes avec une version du client qui peut renvoyer
        # 2 lignes (Version et Python-Version, ref #3658)
        version = version.split('\n')[0]
        # on regarde la version actuelle du client sur zephir
        vers_majmin=version[:version.rindex('-')]
        try:
            version_locale = ""
            if os.path.isdir(os.path.abspath(config.PATH_ZEPHIR)+"/sites/client"):
                rpms=os.listdir(os.path.abspath(config.PATH_ZEPHIR)+"/sites/client")
                for rpm in rpms:
                    # on regarde si on a un paquet correspondant (mm version globale du client)
                    if rpm.startswith(vers_majmin):
                        version_locale=rpm
                        break
            if version_locale == "":
                # on a pas trouvé de rpm de mise à jour
                return 1,"OK"
        except Exception as e:
            return 0,u('erreur lors de la lecture de la version sur zephir : %s' % str(e))
        # on compare avec la version envoyée par le client
        vers_loc = version_locale.replace(vers_majmin,"")
        vers_dist = version.replace(vers_majmin,"")
        if version.split('zephir-client')[1][1:].startswith('2'):
            # cas de eole2 : revision avec X ou Xeol ?
            vers_loc = vers_loc[1:vers_loc.index('_')].replace('eole','')
            vers_dist = vers_dist[1:].replace('eole','')
        else:
            vers_loc = vers_loc[1:vers_loc.index('eol')]
            vers_dist = vers_dist[1:vers_dist.index('eol')]
        # comparaison de la révision
        maj=0
        apt_pkg.init_system()
        if apt_pkg.version_compare(vers_loc, vers_dist) > 0:
            maj=1
        if maj == 1:
            return 1,u(version_locale.strip())
        else:
            return 1,"OK"

    def xmlrpc_list_client(self,cred_user):
        """liste les clients disponibles sur zephir"""
        try:
            rpms = []
            rpm_dir = os.path.abspath(config.PATH_ZEPHIR)+"/sites/client/"
            if os.path.isdir(rpm_dir):
                for rpm in os.listdir(rpm_dir):
                    if rpm.startswith('zephir-client'):
                        rpms.append(rpm)
            return 1, u(rpms)
        except Exception as e:
            return 0, u("Erreur de lecture du répertoire")

    def xmlrpc_update_passwd(self, cred_user, passwd):
        """Met à jour le mot de passe de l'utilisateur connecté.
        Valide uniquement dans le cadre de l'annuaire LDAP local
        """
        try:
            assert config.ADRESSE_LDAP in ('localhost', '127.0.0.1'), "Gestion des mots de passe non disponible"
            # on vérifie le format (SSHA obligatoire)
            passwd = base64.decodebytes(to_bytes(passwd)).decode()
            if not passwd.startswith('{SSHA}'):
                raise ValueError("format de mot de passe non autorisé (SSHA requis)")
            assert cred_user != 'zephir', "l'utilisateur zephir n'est pas modifiable"
            # application du mot de passe
            add_user(cred_user, passwd)
        except Exception as err:
            err_msg = str(err)
            log.msg('Erreur lors du changement de mot de passe utilisateur: %s' % err_msg)
            return 0, u(err_msg)
        return 1, u("Mot de passe modifié pour %s" % cred_user)
