Sommaire

1) le backend XML-RPC

La base de zephir est constituée d'un serveur XML-RPC basé sur la technologie TwistedMatrix (Twisted) et dialoguant avec une base de données postgresql (sur zephir). L'accès aux fonctions du serveur sont protégées par une authentification sur un serveur LDAP (annuaire local fourni avec Zephir ou tout autre annuaire accessible). Accès à l'api du serveur xml-rpc.

1.1) Organisation des méthodes du serveur

Le backend est réparti sur plusieurs fichiers. chacun de ces fichiers correspond à une classe qui comporte un certain nombre de fonctions du backend. Chaque classe donnera une 'branche dans le serveur'.

Ces classes dérivent toutes de la classe XMLRPCEole, qui est une version authentifiée de la classe XMLRPC de TwistedMatrix (voir le paragraphe suivant pour les modifications approtées).

Les fonctions ont été réparties de la façon suivante:

  • users_rpc.py: classe racine du serveur à laquelle sont rattachées toutes les autres. Gère les fonctions telles que la gestion des préférences utilisateurs, la gestion des droits, ...
  • etabs_rpc.py: fonctions de gestion des établissements
  • modules_rpc.py: fonctions de gestion des modules et des variantes
  • serveurs_rpc.py: fonctions de gestion des serveurs et groupes de serveurs
  • services_rpc.py: fonctions de gestion des services (non utilisé actuellement)
  • uucp_rpc.py: fonctions diverses de communication entre zephir et les clients
  • local_rpc.py: classe vide pour l'ajout de fonctions (contributions)

Pour le détail des fonctions présentes dans chaque classe, se reporter à l'api du backend

méthode d'appel distant au backend (avec python). Exemple d'appel de la fonction get_serveur (définie dans etab_rpc.py):

>>> import xmlrpclib
>>> zephir=xmlrpclib.ServerProxy('https://user:passwd@adresse_zephir:7080')
>>> retour=zephir.serveurs.get_serveur(1)
>>> print retour
[1, [{'libelle': 'amon-1.5', 'id': 1}]]

1.2) Authentification LDAP pour le serveur XML-RPC

La méthode retenue pour sécuriser xml-rpc est d'authentifier l'utilisateur par l'intermédiaire des requêtes http. Par exemple, la connexion au serveur se fera par l'appel suivant :

https://toto:password@192.168.230.63:7080/

Dans cet exemple, la librairie http va se charger de crypter le login et le mot de passe. du côté du serveur, les valeurs corespondantes peuvent être récupérées à travers l'objet requête comme suit :

cred_user = request.getUser()
cred_password = request.getPassword()

La solution actuelle consiste à redéfinir les fonctions render() et __init__() de la classe XMLRPC de twisted (fichier /usr/lib/pythonX.X/site-package/twisted/web/xmlrpc). Dans la fonction __init__, on va définir les utilisateurs permis et leurs mot de passe (la version actuelle va chercher les mots de passes dans un annuaire ldap).

On définit également des groupes de fonctions autorisés ou non (stockés dans la base de données) qui définissent quelles fonctions l'utilisateur a le droit d'exécuter sur le serveur.

1.3) Vérification de l'authentification et des autorisations

Tous les utilisateurs présents dans l'annuaire peuvent être utilisés, mais il faut trouver un moyen de les vérifier et d'empêcher l'exécution de code non autorisé.

l'implémentation de ces fonctions se fait au niveau de la fonction render() du serveur, qui est la fonction qui exécute la fonction demandée et retourne le résultat à l'utilisateur. Comme la requête HTTP est accessible dans cette fonction, il suffit de récupérer le login et le mot de passe envoyés dans cette requête et d'effectuer une connexion authentifiée (bind) à l'annuaire.

Si la fonction échoue c'est que le mot de passe est mauvais. Dans le cas ou le nom de login et le mot de passe fournis sont corrects, on peut récupérer le nom de la fonction demandée par l'utilisateur via la fonction suivante:

arguments, nom_fonction = xmlrpclib.loads(request.content.read())

Une fois le nom de la fonction récupérée, on regarde si la fonction est dans les groupes autorisés pour cet utilisateur

# on vérifie si l'utilisateur a le droit d'utiliser cette fonction
# on récupère les groupes de droits de l'utilisateur
cx = PgSQL.connect(database=config.DB_NAME,user=config.DB_USER,password=config.DB_PASSWD)
cursor=cx.cursor()
cursor.execute("""select droits from users where login='%s'""" % cred_user)
rs = cursor.fetchone()
cursor.close()
droits = []
# on rassemble toutes les fonctions auxquelles on a droit
if rs == []:
        groupe = []
else:
        for groupe in eval(rs[0]):
                droits.extend(self.groupes[groupe][1])
try:
        # on regarde si on a le droit d'executer la fonction
        if functionPath not in droits:
                # fonction interdite
                print "\nutilisation de la fonction %s interdite pour %s (%s)"
                                 % (functionPath,cred_user,request.getHost()[1])
                errpage = error.ErrorPage(http.UNAUTHORIZED,
                                          "Unauthorized",
                                          "erreur,ressource %s non autorisée !" % (request.uri))
                return errpage.render(request)
except:
        print "\n pas d'autorisations pour " + cred_user + " !" 
        errpage = error.ErrorPage(http.UNAUTHORIZED,
                                  "Unauthorized",
                                  "erreur, ressource %s non autorisée !" % (request.uri))
        return errpage.render(request)
# fonction autorisée
try:
        function = self._getFunction(functionPath)
except xmlrpc.NoSuchFunction:
        self._cbRender(
                xmlrpclib.Fault(self.NOT_FOUND, "no such function %s" % functionPath),
                request
        )
else:
        request.setHeader("content-type", "text/xml")
        defer.maybeDeferred(function, cred_user, *args).addErrback(
                self._ebRender
        ).addCallback(
                self._cbRender, request
        )
return server.NOT_DONE_YET

Les droits sont stockés dans la base de données postgresql. Chaque utilisateur de l'application est rattaché à un ou plusieurs groupes de droits qui sont affectés par l'administrateur de zephir. Ces groupes contiennent la liste des noms de fonctions du backend qui leur appartiennent (par exemple, 'serveur.get_serveur' et 'get_user' font partie des droits en lecture). les groupes de droits originaux sont dans le fichier '/etc/eole/zephir.sql', et sont insérés dans la base à l'instanciation de zephir (création des données de base).

1.4) Outils divers du backend

  • Les codes d'erreurs

    XML-RPC ne gère qu'une exception générique. De manière à récupérer les différents types d'exception, l'api de communiquation avec le bakend renvoie en premier des codes d'erreur, en général : .. pycode:

    [0,[exception],[msg]]: en cas d'erreur 
    [1, [resultat]]: si tout va bien
    

    Contrairement au status codes unix, bien remarquer qu'ici la convention est 0 pour un code d'erreur, et 1 pour un code retour normal. Ce choix est dû au fait qu'en python, 0 est aussi un booléen équivalent à False. Exception est ici un message du type 'libpq.OperationalError' ou 'libpq.IntegrityError', qui est ensuite convertit dans le frontend en DatabaseError. Si rien n'est renvoyé, le frontend génère une BackendError. msg est le message renvoyé par la base de donnée, de manière, côté frontend, à récupérer le nom de la table qui a provoqué l'erreur.

  • Conversions Unicode/UTF-8

    Le backend encode les paramètres des méthodes invoquées, ainsi que le retour, le résultat des méthodes, en UTF8. Pour pouvoir localiser les données (en encoding UTF-8), il y a des deux côtés des convertisseurs. Côté frontend, pour envoyer des paramètres de méthode xml-rpc, il faut toujours les faire passer par u(), qui convertit en UTF8. La fonction convert() a l'effet inverse, elle permet donc de convertir les données transitant via xml-rpc en encoding utf-8.

  • Gestion du spool Uucp

    Une librairie est disponible pour gérer la file d'attente Uucp. Elle permet de: - connaitre la liste de commandes et de fichiers en attente de transfert pour un serveur (list_files,list_cmd) - ajouter des commandes ou fichiers à la file d'attente (add_cmd,add_file) - supprimer des commandes ou fichiers en attente (remove_cmd) - purger entièrement la file d'attente d'une liste de serveurs (flush)

    cette librairie est /usr/share/zephir/backend/uucp_utils.py

    exemple d'utilisation (Télécharger):

    from zephir.backend.uucp_utils import uucp_pool, UUCPError, COMMANDS
    import sys,time
    
    # id_uucp correspond à l'identifiant du serveur dans la conf uucp
    # (dans zephir : numero_rne-identifiant_serveur)
    id_uucp = "0210001-1"
    
    try:
            # ajout d'un fichier
            print "ajout fichier ..."
            id_file = uucp_pool.add_file(id_uucp,"/tmp/config.rpt")
    
            # ajout d'une commande
            print "ajout de 2 commandes ..."
            id_cmd1 = uucp_pool.add_cmd(id_uucp,"zephir_client configure")
            id_cmd2 = uucp_pool.add_cmd(id_uucp,"zephir_client reconfigure")
            
            # affichage de la file d'attente de ce serveur (on met un petit temps d'attente
            # pour être sur que la commande est bien ajoutée avant l'affichage)
            time.sleep(0.5)
            print "commandes : %s \n fichiers : %s \n" % (uucp_pool.list_cmd(id_uucp),uucp_pool.list_files(id_uucp))
    
            # on supprime le fichier de la file
            print "suppression commande 1 ..."
            uucp_pool.remove_cmd(id_uucp,id_cmd2)
            print "commandes : %s \n fichiers : %s \n" % (uucp_pool.list_cmd(id_uucp),uucp_pool.list_files(id_uucp))
    
            # on purge toutes les commandes
            print "vidage complet ..."
            uucp_pool.flush([id_uucp])
            print "commandes : %s \n fichiers : %s \n" % (uucp_pool.list_cmd(id_uucp),uucp_pool.list_files(id_uucp))
            
    except UUCPError,e:
            sys.exit(("Erreur UUCP %s" % str(e)))