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