From 3589ef41abc3c6daaa1d185a2a3c2a96ce34f791 Mon Sep 17 00:00:00 2001
From: Vincent Le Gallic <legallic@crans.org>
Date: Sat, 13 Apr 2013 07:35:11 +0200
Subject: [PATCH] getfile*s*; putfile*s* et utilisation pour --recrypt-files
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

On peut récupérer/envoyer plusieurs fichiers à la fois.

A priori, le serveur n'est plus rétro-compatible avec les clients non à jour.

Conflicts:
	client.py
	server.py
---
 client.py | 109 ++++++++++++++++++++++++++++++++++--------------------
 server.py |  53 +++++++++++++++++++++-----
 2 files changed, 112 insertions(+), 50 deletions(-)

diff --git a/client.py b/client.py
index a5d1a6a..123a696 100755
--- a/client.py
+++ b/client.py
@@ -21,6 +21,7 @@ import re
 import random
 import string
 import datetime
+import gnupg
 try:
     import gnupg #disponible seulement sous wheezy
 except ImportError:
@@ -68,7 +69,7 @@ CLIPBOARD = bool(os.getenv('DISPLAY')) and os.path.exists('/usr/bin/xclip')
 #: Mode «ne pas demander confirmation»
 FORCED = False
 #: Droits à définir sur le fichier en édition
-NROLES = None
+NEWROLES = None
 #: Serveur à interroger (peuplée à l'exécution)
 SERVER = None
 
@@ -128,10 +129,11 @@ def remote_command(command, arg = None, stdin_contents = None):
     commande"""
     
     sshin, sshout = ssh(command, arg)
-    if stdin_contents:
+    if not stdin_contents is None:
         sshin.write(json.dumps(stdin_contents))
         sshin.close()
-    return json.loads(sshout.read())
+    raw_out = sshout.read()
+    return json.loads(raw_out)
 
 @simple_memoize
 def all_keys():
@@ -148,14 +150,13 @@ def all_files():
     """Récupère les fichiers du serveur distant"""
     return remote_command("listfiles")
 
-def get_file(filename):
-    """Récupère le contenu du fichier distant"""
-    return remote_command("getfile", filename)
+def get_files(filenames):
+    """Récupère le contenu des fichiers distants"""
+    return remote_command("getfiles", stdin_contents=filenames)
 
-def put_file(filename, roles, contents):
-    """Dépose le fichier sur le serveur distant"""
-    return remote_command("putfile", filename, {'roles': roles,
-                                                'contents' : contents})
+def put_files(files):
+    """Dépose les fichiers sur le serveur distant"""
+    return remote_command("putfiles", stdin_contents=files)
 
 def rm_file(filename):
     """Supprime le fichier sur le serveur distant"""
@@ -238,8 +239,7 @@ def get_dest_of_roles(roles):
                for rec in get_recipients_of_roles(roles) if allkeys[rec][1]]
 
 def encrypt(roles, contents):
-    """Chiffre le contenu pour les roles donnés"""
-    
+    """Chiffre ``contents`` pour les ``roles`` donnés"""
     allkeys = all_keys()
     recipients = get_recipients_of_roles(roles)
     
@@ -270,20 +270,20 @@ def put_password(name, roles, contents):
     """Dépose le mot de passe après l'avoir chiffré pour les
     destinataires donnés"""
     success, enc_pwd_or_error = encrypt(roles, contents)
-    if NROLES != None:
-        roles = NROLES
+    if NEWROLES != None:
+        roles = NEWROLES
         if VERB:
             print(u"Pas de nouveaux rôles".encode("utf-8"))
     if success:
         enc_pwd = enc_pwd_or_error
-        return put_file(name, roles, enc_pwd)
+        return put_files([{'filename' : name, 'roles' : roles, 'contents' : enc_pwd}])[0]
     else:
         error = enc_pwd_or_error
         return [False, error]
 
 def get_password(name):
     """Récupère le mot de passe donné par name"""
-    gotit, remotefile = get_file(name)
+    gotit, remotefile = get_files([name])[0]
     if gotit:
         remotefile = decrypt(remotefile['contents'])
     return [gotit, remotefile]
@@ -367,7 +367,7 @@ def clipboard(texte):
 
 def show_file(fname):
     """Affiche le contenu d'un fichier"""
-    gotit, value = get_file(fname)
+    gotit, value = get_files([fname])[0]
     if not gotit:
         print(value.encode("utf-8")) # value contient le message d'erreur
         return
@@ -385,7 +385,7 @@ def show_file(fname):
             line = clipboard(catchPass.group(1))
         ntexte += line + '\n'
     showbin = "cat" if hidden else "less"
-    proc = subprocess.Popen(showbin, stdin=subprocess.PIPE, shell=True)
+    proc = subprocess.Popen([showbin], stdin=subprocess.PIPE)
     out = proc.stdin
     raw = u"Fichier %s:\n\n%s-----\nVisible par: %s\n" % (fname, ntexte, ','.join(value['roles']))
     out.write(raw.encode("utf-8"))
@@ -395,7 +395,7 @@ def show_file(fname):
         
 def edit_file(fname):
     """Modifie/Crée un fichier"""
-    gotit, value = get_file(fname)
+    gotit, value = get_files([fname])[0]
     nfile = False
     annotations = u""
     if not gotit and not "pas les droits" in value:
@@ -423,7 +423,8 @@ Enregistrez le fichier vide pour annuler.\n"""
         sin.write(value['contents'].encode("utf-8"))
         sin.close()
         texte = sout.read().decode("utf-8")
-    value['roles'] = NROLES or value['roles']
+    # On récupère les nouveaux roles si ils ont été précisés, sinon on garde les mêmes
+    value['roles'] = NEWROLES or value['roles']
     
     annotations += u"""Ce fichier sera chiffré pour les rôles suivants :\n%s\n
 C'est-à-dire pour les utilisateurs suivants :\n%s""" % (
@@ -433,7 +434,7 @@ C'est-à-dire pour les utilisateurs suivants :\n%s""" % (
         
     ntexte = editor(texte, annotations)
     
-    if ((not nfile and ntexte in [u'', texte] and NROLES == None) or # Fichier existant vidé ou inchangé
+    if ((not nfile and ntexte in [u'', texte] and NEWROLES == None) or # Fichier existant vidé ou inchangé
         (nfile and ntexte == u'')):                                  # Nouveau fichier créé vide
         print(u"Pas de modification effectuée".encode("utf-8"))
     else:
@@ -470,26 +471,50 @@ def my_update_keys():
 
 def recrypt_files():
     """Rechiffre les fichiers"""
-    roles = None
+    # Ici, la signification de NEWROLES est : on ne veut rechiffrer que les fichiers qui ont au moins un de ces roles
+    rechiffre_roles = NEWROLES
     my_roles = get_my_roles()
-    if roles == None:
-        # On ne conserve que les rôles qui finissent par -w
-        roles = [ r[:-2] for r in my_roles if r.endswith('-w')]
-    if type(roles) != list:
-        roles = [roles]
-
-    for (fname, froles) in all_files().iteritems():
-        if set(roles).intersection(froles) == set([]):
-            continue
-        print((u"Rechiffrement de %s" % fname).encode("utf-8"))
-        _, password = get_password(fname)
-        put_password(fname, froles, password)
+    my_roles_w = [r for r in my_roles if r.endswith("-w")]
+    if rechiffre_roles == None:
+        # Sans précisions, on prend tous les roles qu'on peut
+        rechiffre_roles = my_roles
+    # On ne conserve que les rôles en écriture
+    rechiffre_roles = [ r[:-2] for r in rechiffre_roles if r.endswith('-w')]
+    
+    # La liste des fichiers
+    allfiles = all_files()
+    # On ne demande que les fichiers dans lesquels on peut écrire
+    # et qui ont au moins un role dans ``roles``
+    askfiles = [filename for (filename, fileroles) in allfiles.iteritems()
+                         if set(fileroles).intersection(roles) != set()
+                         and set(fileroles).intersection(my_roles_w) != set()]
+    files = get_files(askfiles)
+    # Au cas où on aurait échoué à récupérer ne serait-ce qu'un de ces fichiers,
+    # on affiche le message d'erreur correspondant et on abandonne.
+    for (success, message) in files:
+        if not success:
+            print(message.encode("utf-8"))
+            return
+    # On rechiffre
+    to_put = [{'filename' : f['filename'],
+               'roles' : f['roles'],
+               'contents' : encrypt(f['roles'], decrypt(f['contents']))}
+              for f in files]
+    if to_put:
+        print((u"Rechiffrement de %s" % (", ".join([f['filename'] for f in to_put]))).encode("utf-8"))
+        results = put_files(to_put)
+        # On affiche les messages de retour
+        for i in range(len(results)):
+            print (u"%s : %s" % (to_put[i]['filename'], results[i][1]))
+    else:
+        print(u"Aucun fichier n'a besoin d'être rechiffré".encode("utf-8"))
 
 def parse_roles(strroles):
-    """Interprête une liste de rôles fournie par l'utilisateur"""
+    """Interprête une liste de rôles fournie par l'utilisateur.
+       Renvoie ``False`` si au moins un de ces rôles pose problème."""
     if strroles == None: return None
     roles = all_roles()
-    my_roles = filter(lambda r: SERVER['user'] in roles[r],roles.keys())
+    my_roles = filter(lambda r: SERVER['user'] in roles[r], roles.keys())
     my_roles_w = [ r[:-2] for r in my_roles if r.endswith('-w') ]
     ret = set()
     writable = False
@@ -555,10 +580,14 @@ if __name__ == "__main__":
         help="Lister les serveurs")
     action_grp.add_argument('--recrypt-files', action='store_const', dest='action',
         default=show_file, const=recrypt_files,
-        help="Rechiffrer les mots de passe")
+        help="Rechiffrer les mots de passe. (Avec les mêmes rôles qu'avant, sert à rajouter un lecteur)")
 
     parser.add_argument('--roles', nargs='?', default=None,
-        help="liste des roles à affecter au fichier")
+        help="""Liste de roles (séparés par des virgules).
+                Avec --edit, le fichier sera chiffré pour exactement ces roles
+                (par défaut, tous vos rôles en écriture seront utilisés).
+                Avec --recrypt-files, tous les fichiers ayant au moins un de ces roles (et pour lesquels vous avez le droit d'écriture) seront rechiffrés
+                (par défaut, tous les fichiers pour lesquels vous avez les droits en écriture sont rechiffrés).""")
     parser.add_argument('fname', nargs='?', default=None,
         help="Nom du fichier à afficher")
     
@@ -569,9 +598,9 @@ if __name__ == "__main__":
     if parsed.clipboard != None:
         CLIPBOARD = parsed.clipboard
     FORCED = parsed.force
-    NROLES = parse_roles(parsed.roles)
+    NEWROLES = parse_roles(parsed.roles)
     
-    if NROLES != False:
+    if NEWROLES != False:
         if parsed.action.func_code.co_argcount == 0:
             parsed.action()
         elif parsed.fname == None:
diff --git a/server.py b/server.py
index 5d7d4fc..4b30203 100755
--- a/server.py
+++ b/server.py
@@ -73,22 +73,20 @@ def getfile(filename):
         obj = json.loads(open(filepath).read())
         if not validate(obj['roles']):
 	        return [False, u"Vous n'avez pas les droits de lecture sur le fichier %s." % filename]
+        obj["filename"] = filename
         return [True, obj]
     except IOError:
         return [False, u"Le fichier %s n'existe pas." % filename]
      
 
-def putfile(filename):
-    """Écrit le fichier ``filename`` avec les données reçues sur stdin."""
-    filepath = getpath(filename)
+def getfiles():
+    """Récupère plusieurs fichiers, lit la liste des filenames demandés sur stdin"""
     stdin = sys.stdin.read()
-    parsed_stdin = json.loads(stdin)
-    try:
-        roles = parsed_stdin['roles']
-        contents = parsed_stdin['contents']
-    except KeyError:
-        return [False, u"Entrée invalide"]
-    
+    filenames = json.loads(stdin)
+    return [getfile(f) for f in filenames]
+
+def _putfile(filename, roles, contents):
+    """Écrit ``contents`` avec les roles ``roles`` dans le fichier ``filename``"""
     gotit, old = getfile(filename)
     if not gotit:
         old = u"[Création du fichier]"
@@ -102,9 +100,38 @@ def putfile(filename):
     backup(corps, filename, old)
     notification(u"Modification de %s" % filename, corps, filename, old)
     
+    filepath = getpath(filename)
     writefile(filepath, json.dumps({'roles': roles, 'contents': contents}))
     return [True, u"Modification effectuée."]
 
+def putfile(filename):
+    """Écrit le fichier ``filename`` avec les données reçues sur stdin."""
+    stdin = sys.stdin.read()
+    parsed_stdin = json.loads(stdin)
+    try:
+        roles = parsed_stdin['roles']
+        contents = parsed_stdin['contents']
+    except KeyError:
+        return [False, u"Entrée invalide"]
+    return _putfile(filename, roles, contents)
+
+def putfiles():
+    """Écrit plusieurs fichiers. Lit les filenames sur l'entrée standard avec le reste."""
+    stdin = sys.stdin.read()
+    parsed_stdin = json.loads(stdin)
+    results = []
+    for fichier in parsed_stdin:
+        try:
+            filename = fichier['filename']
+            roles = fichier['roles']
+            contents = fichier['contents']
+        except KeyError:
+            results.append([False, u"Entrée invalide"])
+        else:
+            results.append(_putfile(filename, roles, contents))
+    return results
+
+
 def rmfile(filename):
     """Supprime le fichier filename après avoir vérifié les droits sur le fichier"""
     gotit, old = getfile(filename)
@@ -120,6 +147,7 @@ def rmfile(filename):
         return u"Vous n'avez pas les droits d'écriture sur le fichier %s." % filename
     return u"Suppression effectuée"
 
+
 def backup(corps, fname, old):
     """Backupe l'ancienne version du fichier"""
     back = open(getpath(fname, backup=True), 'a')
@@ -168,8 +196,13 @@ if __name__ == "__main__":
         answer = listkeys()
     elif command == "listfiles":
         answer = listfiles()
+    elif command == "getfiles":
+        answer = getfiles()
+    elif command == "putfiles":
+        answer = putfiles()
     else:
         if not filename:
+            print("filename nécessaire pour cette opération", file=sys.stderr)
             sys.exit(1)
         if command == "getfile":
             answer = getfile(filename)
-- 
GitLab