From ab2f04c60f39ede337e506337737e796649c462f Mon Sep 17 00:00:00 2001 From: Vincent Le Gallic <legallic@crans.org> Date: Tue, 30 Jul 2013 04:51:37 +0200 Subject: [PATCH] =?UTF-8?q?On=20vire=20toutes=20les=20variables=20globales?= =?UTF-8?q?,=20donc=20on=20passe=20les=20options=20pars=C3=A9es=20=C3=A0?= =?UTF-8?q?=20quasiment=20toutes=20les=20fonctions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le paramètre --drop-invalid fait son apparition pout droper automatiquement les bad guy-e-s qui ont laissé leur clés expirer. NB : on n'a toujours pas réglé le problème des *sub*keys expirées. --- bash_completion | 2 +- client.py | 446 ++++++++++++++++++++++++++---------------------- 2 files changed, 246 insertions(+), 202 deletions(-) diff --git a/bash_completion b/bash_completion index c50e429..77b32bf 100644 --- a/bash_completion +++ b/bash_completion @@ -23,7 +23,7 @@ _cranspasswords_completion(){ cur="${COMP_WORDS[argc]}" cur_first_char=${cur:0:1} opts_short="-h -v -c -f -l" - opts="--help --server --verbose --quiet --clipboard --noclipboard --force --edit --view --remove --list --check-keys --update-keys --list-roles --recrypt-files --roles --list-servers" + opts="--help --server --verbose --quiet --clipboard --noclipboard --force --drop-invalid --edit --view --remove --list --check-keys --update-keys --list-roles --recrypt-files --roles --list-servers" mkdir -p -m 700 "$role_dir" mkdir -p -m 700 "$pass_dir" diff --git a/client.py b/client.py index cef69c8..b4814a2 100755 --- a/client.py +++ b/client.py @@ -35,22 +35,10 @@ except ImportError: sys.stderr.write(u"Va lire le fichier README.\n".encode("utf-8")) sys.exit(1) -#: pattern utilisé pour détecter la ligne contenant le mot de passe dans les fichiers -PASS = re.compile('[\t ]*pass(?:word)?[\t ]*:[\t ]*(.*)\r?\n?$', +#: Pattern utilisé pour détecter la ligne contenant le mot de passe dans les fichiers +pass_regexp = re.compile('[\t ]*pass(?:word)?[\t ]*:[\t ]*(.*)\r?\n?$', flags=re.IGNORECASE) -## Conf qu'il faudrait éliminer en passant ``parsed`` aux fonctions -#: Mode verbeux -VERB = False -#: Par défaut, place-t-on le mdp dans le presse-papier ? -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 -NEWROLES = None -#: Serveur à interroger (peuplée à l'exécution) -SERVER = None - ## GPG Definitions #: Path du binaire gpg GPG = '/usr/bin/gpg' @@ -78,14 +66,14 @@ GPG_TRUSTLEVELS = { u"q" : (u"non définie", False), } -def gpg(command, args=None, verbose=False): +def gpg(command, args=None): """Lance gpg pour la commande donnée avec les arguments donnés. Renvoie son entrée standard et sa sortie standard.""" full_command = [GPG] full_command.extend(GPG_ARGS[command]) if args: full_command.extend(args) - if verbose or VERB: + if options.verbose: stderr = sys.stderr else: stderr = subprocess.PIPE @@ -95,7 +83,7 @@ def gpg(command, args=None, verbose=False): stdout = subprocess.PIPE, stderr = stderr, close_fds = True) - if not (verbose or VERB): + if not options.verbose: proc.stderr.close() return proc.stdin, proc.stdout @@ -230,19 +218,23 @@ class simple_memoize(object): self.f = f self.val = None - def __call__(self): + def __call__(self, *args, **kwargs): + """Attention ! On peut fournir des paramètres, mais comme on mémorise pour la prochaine fois, + si on rappelle avec des paramètres différents, on aura quand même la même réponse. + Pour l'instant, on s'en fiche puisque les paramètres ne changent pas d'un appel au suivant, + mais il faudra s'en préoccuper si un jour on veut changer le comportement.""" if self.val == None: - self.val = self.f() + self.val = self.f(*args, **kwargs) return self.val ###### ## Remote commands -def ssh(command, arg = None): +def ssh(command, options, arg=None): """Lance ssh avec les arguments donnés. Renvoie son entrée standard et sa sortie standard.""" - full_command = list(SERVER['server_cmd']) + full_command = list(options.serverdata['server_cmd']) full_command.append(command) if arg: full_command.append(arg) @@ -253,11 +245,11 @@ def ssh(command, arg = None): close_fds = True) return proc.stdin, proc.stdout -def remote_command(command, arg = None, stdin_contents = None): +def remote_command(options, command, arg=None, stdin_contents=None): """Exécute la commande distante, et retourne la sortie de cette commande""" - sshin, sshout = ssh(command, arg) + sshin, sshout = ssh(command, options, arg) if not stdin_contents is None: sshin.write(json.dumps(stdin_contents)) sshin.close() @@ -265,37 +257,39 @@ def remote_command(command, arg = None, stdin_contents = None): return json.loads(raw_out) @simple_memoize -def all_keys(): +def all_keys(options): """Récupère les clés du serveur distant""" - return remote_command("listkeys") + return remote_command(options, "listkeys") @simple_memoize -def all_roles(): +def all_roles(options): """Récupère les roles du serveur distant""" - return remote_command("listroles") + return remote_command(options, "listroles") @simple_memoize -def all_files(): +def all_files(options): """Récupère les fichiers du serveur distant""" - return remote_command("listfiles") + return remote_command(options, "listfiles") -def get_files(filenames): +def get_files(options, filenames): """Récupère le contenu des fichiers distants""" - return remote_command("getfiles", stdin_contents=filenames) + return remote_command(options, "getfiles", stdin_contents=filenames) -def put_files(files): +def put_files(options, files): """Dépose les fichiers sur le serveur distant""" - return remote_command("putfiles", stdin_contents=files) + return remote_command(options, "putfiles", stdin_contents=files) def rm_file(filename): """Supprime le fichier sur le serveur distant""" - return remote_command("rmfile", filename) + return remote_command(options, "rmfile", filename) @simple_memoize -def get_my_roles(): - """Retourne la liste des rôles de l'utilisateur""" - allr = all_roles() - return filter(lambda role: SERVER['user'] in allr[role], allr.keys()) +def get_my_roles(options): + """Retourne la liste des rôles de l'utilisateur, et également la liste des rôles dont il possède le role-w.""" + allroles = all_roles(options) + my_roles = [r for (r, users) in allroles.iteritems() if options.serverdata['user'] in users] + my_roles_w = [r[:-2] for r in my_roles if r.endswith("-w")] + return (my_roles, my_roles_w) def gen_password(): """Génère un mot de passe aléatoire""" @@ -307,74 +301,76 @@ def gen_password(): ###### ## Local commands -def update_keys(): +def update_keys(options): """Met à jour les clés existantes""" - keys = all_keys() + keys = all_keys(options) _, stdout = gpg("receive-keys", [key for _, key in keys.values() if key]) return stdout.read().decode("utf-8") -def check_keys(recipients=None, interactive=False, drop_invalid=False): +def check_keys(options, recipients=None, quiet=False): """Vérifie les clés, c'est-à -dire, si le mail est présent dans les identités du fingerprint, et que la clé est de confiance (et non expirée/révoquée). * Si ``recipients`` est fourni, vérifie seulement ces recipients. Renvoie la liste de ceux qu'on n'a pas droppés. - * Si ``interactive=True``, demandera confirmation pour dropper un recipient dont la clé est invalide. - * Sinon, et si ``drop_invalid=True``, droppe les recipients automatiquement. + * Si ``options.force=False``, demandera confirmation pour dropper un recipient dont la clé est invalide. + * Sinon, et si ``options.drop_invalid=True``, droppe les recipients automatiquement. * Si rien n'est fourni, vérifie toutes les clés et renvoie juste un booléen disant si tout va bien. """ - if QUIET: - interactive = False trusted_recipients = [] - keys = all_keys() + keys = all_keys(options) if recipients is None: - SPEAK = VERB + speak = options.verbose and not options.quiet else: - SPEAK = False + speak = False keys = {u : val for (u, val) in keys.iteritems() if u in recipients} - if SPEAK: + if speak: print("M : le mail correspond à un uid du fingerprint\nC : confiance OK (inclut la vérification de non expiration).\n") _, gpgout = gpg('list-keys') localring = parse_keys(gpgout) for (recipient, (mail, fpr)) in keys.iteritems(): failed = u"" if not fpr is None: - if SPEAK: + if speak: print((u"Checking %s… " % (mail)).encode("utf-8"), end="") key = localring.get(fpr, None) # On vérifie qu'on possède la clé… if not key is None: # …qu'elle correspond au mail… if any([u"<%s>" % (mail,) in u["uid"] for u in key["uids"]]): - if SPEAK: + if speak: print("M ", end="") meaning, trustvalue = GPG_TRUSTLEVELS[key["trustletter"]] # … et qu'on lui fait confiance if not trustvalue: failed = u"La confiance en la clé est : %s" % (meaning,) - elif SPEAK: + elif speak: print("C ", end="") else: failed = u"!! Le fingerprint et le mail ne correspondent pas !" else: failed = u"Pas (ou trop) de clé avec ce fingerprint." - if SPEAK: + if speak: print("") if failed: - if not QUIET: + if not options.quiet: print((u"--> Fail on %s:%s\n--> %s" % (mail, fpr, failed)).encode("utf-8")) if not recipients is None: # On cherche à savoir si on droppe ce recipient - drop = True # par défaut, on le drope - if interactive: - if not confirm(u"Abandonner le chiffrement pour cette clé ? (Si vous la conservez, il est posible que gpg crashe)"): - drop = False # sauf si on a répondu non à "abandonner ?" - elif not drop_invalid: + message = u"Abandonner le chiffrement pour cette clé ? (Si vous la conservez, il est posible que gpg crashe)" + if not confirm(options, message): + drop = False # si on a répondu non à "abandonner ?", on droppe pas + elif not options.drop_invalid: drop = False # ou bien si drop_invalid ne nous autorise pas à le dropper silencieusement + else: + drop = True # Là , on peut dropper if not drop: trusted_recipients.append(recipient) + else: + if not options.quiet: + print(u"Droppe la clé %s:%s" % (fpr, recipient)) else: trusted_recipients.append(recipient) if recipients is None: @@ -382,27 +378,27 @@ def check_keys(recipients=None, interactive=False, drop_invalid=False): else: return trusted_recipients -def get_recipients_of_roles(roles): +def get_recipients_of_roles(options, roles): """Renvoie les destinataires d'un rôle""" recipients = set() - allroles = all_roles() + allroles = all_roles(options) for role in roles: for recipient in allroles[role]: recipients.add(recipient) return recipients -def get_dest_of_roles(roles): +def get_dest_of_roles(options, roles): """Renvoie la liste des "username : mail (fingerprint)" """ - allkeys = all_keys() + allkeys = all_keys(options) return [u"%s : %s (%s)" % (rec, allkeys[rec][0], allkeys[rec][1]) - for rec in get_recipients_of_roles(roles) if allkeys[rec][1]] + for rec in get_recipients_of_roles(options, roles) if allkeys[rec][1]] -def encrypt(roles, contents, interactive_trust=True, drop_invalid=False): +def encrypt(options, roles, contents): """Chiffre le contenu pour les roles donnés""" - allkeys = all_keys() - recipients = get_recipients_of_roles(roles) - recipients = check_keys(recipients, interactive=interactive_trust, drop_invalid=drop_invalid) + allkeys = all_keys(options) + recipients = get_recipients_of_roles(options, roles) + recipients = check_keys(options, recipients=recipients, quiet=True) fpr_recipients = [] for recipient in recipients: fpr = allkeys[recipient][1] @@ -426,31 +422,27 @@ def decrypt(contents): stdin.close() return stdout.read().decode("utf-8") -def put_password(name, roles, contents, interactive_trust=True, drop_invalid=False): +def put_password(options, 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, interactive_trust, drop_invalid) - if NEWROLES != None: - roles = NEWROLES - if VERB: - print(u"Pas de nouveaux rôles".encode("utf-8")) + destinataires dans ``roles``.""" + success, enc_pwd_or_error = encrypt(options, roles, contents) if success: enc_pwd = enc_pwd_or_error - return put_files([{'filename' : name, 'roles' : roles, 'contents' : enc_pwd}])[0] + return put_files(options, [{'filename' : options.fname, '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_files([name])[0] - if gotit: - remotefile = decrypt(remotefile['contents']) - return [gotit, remotefile] - ###### ## Interface +NEED_FILENAME = [] + +def need_filename(f): + """Décorateur qui ajoutera la fonction à la liste des fonctions qui attendent un filename.""" + NEED_FILENAME.append(f) + return f + def editor(texte, annotations=u""): """ Lance $EDITOR sur texte. Renvoie le nouveau texte si des modifications ont été apportées, ou None @@ -472,11 +464,11 @@ def editor(texte, annotations=u""): ntexte = u'\n'.join(filter(lambda l: not l.startswith('#'), ntexte.split('\n'))) return ntexte -def show_files(): +def show_files(options): """Affiche la liste des fichiers disponibles sur le serveur distant""" print(u"Liste des fichiers disponibles :".encode("utf-8")) - my_roles = get_my_roles() - files = all_files() + my_roles, _ = get_my_roles(options) + files = all_files(options) keys = files.keys() keys.sort() for fname in keys: @@ -485,23 +477,22 @@ def show_files(): print((u" %s %s (%s)" % ((access and '+' or '-'), fname, ", ".join(froles))).encode("utf-8")) print((u"""--Mes roles: %s""" % (", ".join(my_roles),)).encode("utf-8")) -def show_roles(): +def show_roles(options): """Affiche la liste des roles existants""" print(u"Liste des roles disponibles".encode("utf-8")) - for (role, usernames) in all_roles().iteritems(): + for (role, usernames) in all_roles(options).iteritems(): if not role.endswith('-w'): print((u" * %s : %s" % (role, ", ".join(usernames))).encode("utf-8")) -def show_servers(): +def show_servers(options): """Affiche la liste des serveurs disponibles""" print(u"Liste des serveurs disponibles".encode("utf-8")) for server in config.servers.keys(): print((u" * " + server).encode("utf-8")) old_clipboard = None -def saveclipboard(restore=False): +def saveclipboard(restore=False, old_clipboard=None): """Enregistre le contenu du presse-papier. Le rétablit si ``restore=True``""" - global old_clipboard if restore and old_clipboard == None: return act = '-in' if restore else '-out' @@ -514,97 +505,121 @@ def saveclipboard(restore=False): proc.stdin.write(old_clipboard) proc.stdin.close() proc.stdout.close() + return old_clipboard def clipboard(texte): """Place ``texte`` dans le presse-papier en mémorisant l'ancien contenu.""" - saveclipboard() + old_clipboard = saveclipboard() proc =subprocess.Popen(['xclip', '-selection', 'clipboard'],\ stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr) proc.stdin.write(texte.encode("utf-8")) proc.stdin.close() - return u"[Le mot de passe a été mis dans le presse papier]" + return old_clipboard - -def show_file(fname): +@need_filename +def show_file(options): """Affiche le contenu d'un fichier""" - gotit, value = get_files([fname])[0] + fname = options.fname + gotit, value = get_files(options, [fname])[0] if not gotit: - print(value.encode("utf-8")) # value contient le message d'erreur + if not options.quiet: + print(value.encode("utf-8")) # value contient le message d'erreur return + passfile = value (sin, sout) = gpg('decrypt') - sin.write(value['contents'].encode("utf-8")) + sin.write(passfile['contents'].encode("utf-8")) sin.close() texte = sout.read().decode("utf-8") ntexte = u"" hidden = False # Est-ce que le mot de passe a été caché ? lines = texte.split('\n') for line in lines: - catchPass = PASS.match(line) - if catchPass != None and CLIPBOARD: + catchPass = pass_regexp.match(line) + if catchPass != None and options.clipboard: hidden = True - line = clipboard(catchPass.group(1)) + # On met le mdp dans le clipboard en mémorisant sont ancien contenu + old_clipboard = clipboard(catchPass.group(1)) + # Et donc on override l'affichage + line = u"[Le mot de passe a été mis dans le presse papier]" ntexte += line + '\n' showbin = "cat" if hidden else "less" 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'])) + raw = u"Fichier %s:\n\n%s-----\nVisible par: %s\n" % (fname, ntexte, ','.join(passfile['roles'])) out.write(raw.encode("utf-8")) out.close() os.waitpid(proc.pid, 0) + if options.clipboard: + saveclipboard(restore=True, old_clipboard=old_clipboard) - -def edit_file(fname, interactive_trust=True, drop_invalid=False): +@need_filename +def edit_file(options): """Modifie/Crée un fichier""" - gotit, value = get_files([fname])[0] + fname = options.fname + gotit, value = get_files(options, [fname])[0] nfile = False annotations = u"" - if not gotit and not "pas les droits" in value: + if not gotit and not u"pas les droits" in value: nfile = True - print(u"Fichier introuvable".encode("utf-8")) - if not confirm(u"Créer fichier ?"): + if not options.quiet: + print(u"Fichier introuvable".encode("utf-8")) + if not confirm(options, u"Créer fichier ?"): return annotations += u"""Ceci est un fichier initial contenant un mot de passe aléatoire, pensez à rajouter une ligne "login: ${login}" Enregistrez le fichier vide pour annuler.\n""" texte = u"pass: %s\n" % gen_password() - roles = get_my_roles() - # Par défaut les roles d'un fichier sont ceux en écriture de son - # créateur - roles = [ r[:-2] for r in roles if r.endswith('-w') ] - if roles == []: - print(u"Vous ne possédez aucun rôle en écriture ! Abandon.".encode("utf-8")) + if options.roles == []: + if not options.quiet: + print(u"Vous ne possédez aucun rôle en écriture ! Abandon.".encode("utf-8")) return - value = {'roles' : roles} + passfile = {'roles' : options.roles} elif not gotit: - print(value.encode("utf-8")) # value contient le message d'erreur + if not options.quiet: + print(value.encode("utf-8")) # value contient le message d'erreur return else: + passfile = value (sin, sout) = gpg('decrypt') - sin.write(value['contents'].encode("utf-8")) + sin.write(passfile['contents'].encode("utf-8")) sin.close() texte = sout.read().decode("utf-8") + # On peut vouloir chiffrer un fichier sans avoir la possibilité de le lire dans le futur + # Mais dans ce cas on préfère demander confirmation + my_roles, _ = get_my_roles(options) + if not options.force and set(options.roles).intersection(my_roles) == set(): + message = u"""Vous vous apprêtez à perdre vos droits de lecture (ROLES ne contient rien parmi : %s) sur ce fichier, continuer ?""" + message = message % (", ".join(my_roles),) + if not confirm(options, message): + sys.exit(2) # 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'] + passfile['roles'] = options.roles or passfile['roles'] annotations += u"""Ce fichier sera chiffré pour les rôles suivants :\n%s\n C'est-à -dire pour les utilisateurs suivants :\n%s""" % ( - ', '.join(value['roles']), - '\n'.join(' %s' % rec for rec in get_dest_of_roles(value['roles'])) + ', '.join(passfile['roles']), + '\n'.join(' %s' % rec for rec in get_dest_of_roles(options, passfile['roles'])) ) - + ntexte = editor(texte, annotations) - 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")) + if ((not nfile and ntexte in [u'', texte] # pas nouveau, vidé ou pas modifié + and set(options.roles) == set(passfile['roles'])) # et on n'a même pas touché à ses rôles, + or (nfile and ntexte == u'')): # ou alors on a créé un fichier vide. + message = u"Pas de modification à enregistrer.\n" + message += u"Si ce n'est pas un nouveau fichier, il a été vidé ou n'a pas été modifié (même pas ses rôles).\n" + message += u"Si c'est un nouveau fichier, vous avez tenté de le créer vide." + if not options.quiet: + print(message.encode("utf-8")) else: ntexte = texte if ntexte == None else ntexte - success, message = put_password(fname, value['roles'], ntexte, interactive_trust, drop_invalid) + success, message = put_password(options, passfile['roles'], ntexte) print(message.encode("utf-8")) -def confirm(text): - """Demande confirmation, sauf si on est mode ``FORCED``""" - if FORCED: return True +def confirm(options, text): + """Demande confirmation, sauf si on est mode ``--force``""" + if options.force: + return True while True: out = raw_input((text + u' (o/n)').encode("utf-8")).lower() if out == 'o': @@ -612,102 +627,127 @@ def confirm(text): elif out == 'n': return False -def remove_file(fname): +@need_filename +def remove_file(options): """Supprime un fichier""" - if not confirm(u'Êtes-vous sûr de vouloir supprimer %s ?' % fname): + fname = options.fname + if not confirm(options, u'Êtes-vous sûr de vouloir supprimer %s ?' % (fname,), options): return message = rm_file(fname) print(message.encode("utf-8")) -def my_check_keys(): +def my_check_keys(options): """Vérifie les clés et affiche un message en fonction du résultat""" print(u"Vérification que les clés sont valides (uid correspondant au login) et de confiance.") - print((check_keys() and u"Base de clés ok" or u"Erreurs dans la base").encode("utf-8")) + print((check_keys(options) and u"Base de clés ok" or u"Erreurs dans la base").encode("utf-8")) -def my_update_keys(): +def my_update_keys(options): """Met à jour les clés existantes et affiche le résultat""" - print(update_keys().encode("utf-8")) - -def recrypt_files(interactive_trust=False, drop_invalid=True): - """Rechiffre les fichiers""" - # 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() - my_roles_w = [r for r in my_roles if r.endswith("-w")] + print(update_keys(options).encode("utf-8")) + +def recrypt_files(options): + """Rechiffre les fichiers. + Ici, la signification de ``options.roles`` est : on ne veut rechiffrer que les fichiers qui ont au moins un de ces roles. + """ + rechiffre_roles = options.roles + _, my_roles_w = get_my_roles(options) 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')] + rechiffre_roles = my_roles_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`` + allfiles = all_files(options) + # On ne demande que les fichiers qui ont au moins un role dans ``options.roles`` + # et dans lesquels on peut écrire askfiles = [filename for (filename, fileroles) in allfiles.iteritems() - if set(fileroles).intersection(roles) != set() + if set(fileroles).intersection(options.roles) != set() and set(fileroles).intersection(my_roles_w) != set()] - files = get_files(askfiles) + files = get_files(options, 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")) + if not options.quiet: + print(message.encode("utf-8")) return + # On informe l'utilisateur et on demande confirmation avant de rechiffrer + # Si il a précisé --force, on ne lui demandera rien. + filenames = ", ".join(askfiles) + message = u"Vous vous apprêtez à rechiffrer les fichiers suivants :\n%s" % filenames + if not confirm(options, message + u"\nConfirmer"): + sys.exit(2) # On rechiffre to_put = [{'filename' : f['filename'], 'roles' : f['roles'], - 'contents' : encrypt(f['roles'], decrypt(f['contents']))} + 'contents' : encrypt(options, 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")) + if not options.quiet: + 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])) + if not options.quiet: + 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. - 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_w = [ r[:-2] for r in my_roles if r.endswith('-w') ] + if not options.quiet: + print(u"Aucun fichier n'a besoin d'être rechiffré".encode("utf-8")) + +def parse_roles(options): + """Interprête la liste de rôles fournie par l'utilisateur. + Si il n'en a pas fourni, on considère qu'il prend tous ceux pour lesquels il a le -w. + + Renvoie ``False`` si au moins un de ces rôles pose problème. + + poser problème, c'est : + * être un role-w (il faut utiliser le role sans le w) + * ne pas exister dans la config du serveur + + """ + strroles = options.roles + allroles = all_roles(options) + _, my_roles_w = get_my_roles(options) + if strroles == None: + # L'utilisateur n'a rien donné, on lui donne les rôles (non -w) dont il possède le -w + return my_roles_w ret = set() - writable = False for role in strroles.split(','): - if role not in roles.keys(): - print((u"Le rôle %s n'existe pas !" % role).encode("utf-8")) - return False + if role not in allroles.keys(): + if not options.quiet: + print((u"Le rôle %s n'existe pas !" % role).encode("utf-8")) + sys.exit(1) if role.endswith('-w'): - print((u"Le rôle %s ne devrait pas être utilisé ! (utilisez %s)") - % (role, role[:-2])).encode("utf-8") - return False - writable = writable or role in my_roles_w + if not options.quiet: + print((u"Le rôle %s ne devrait pas être utilisé ! (utilisez %s)") + % (role, role[:-2])).encode("utf-8") + sys.exit(1) ret.add(role) - - if not FORCED and not writable: - if not confirm(u"""Vous vous apprêtez à perdre vos droits d'écritures\ -(ROLES ne contient pas %s) sur ce fichier, continuer ?""" % - ", ".join(my_roles_w)): - return False return list(ret) +def insult_on_nofilename(options, parser): + """Insulte (si non quiet) et quitte si aucun nom de fichier n'a été fourni en commandline.""" + if options.fname == None: + if not options.quiet: + print(u"Vous devez fournir un nom de fichier avec cette commande".encode("utf-8")) + parser.print_help() + sys.exit(1) + if __name__ == "__main__": - parser = argparse.ArgumentParser(description="trousseau crans") + parser = argparse.ArgumentParser(description="Gestion de mots de passe partagés grâce à GPG.") parser.add_argument('-s', '--server', default='default', help="Utilisation d'un serveur alternatif (test, backup, etc)") parser.add_argument('-v', '--verbose', action='store_true', default=False, help="Mode verbeux") + parser.add_argument('--drop-invalid', action='store_true', default=False, + dest='drop_invalid', + help="Combiné avec --force, droppe les clés en lesquelles on n'a pas confiance sans demander confirmation.") parser.add_argument('-q', '--quiet', action='store_true', default=False, - help="Mode silencieux. Cache les message d'erreurs (override --verbose).") + help="Mode silencieux. Cache les message d'erreurs (override --verbose et --interactive).") parser.add_argument('-c', '--clipboard', action='store_true', default=None, help="Stocker le mot de passe dans le presse papier") parser.add_argument('--no-clip', '--noclip', '--noclipboard', action='store_false', default=None, - dest='clipboard', + dest='noclipboard', help="Ne PAS stocker le mot de passe dans le presse papier") parser.add_argument('-f', '--force', action='store_true', default=False, help="Ne pas demander confirmation") @@ -743,7 +783,7 @@ if __name__ == "__main__": help="""Rechiffrer les mots de passe. (Avec les mêmes rôles que ceux qu'ils avant. Cela sert à mettre à jour les recipients pour qui un password est chiffré)""") - + parser.add_argument('--roles', nargs='?', default=None, help="""Liste de roles (séparés par des virgules). Avec --edit, le fichier sera chiffré pour exactement ces roles @@ -753,25 +793,29 @@ if __name__ == "__main__": parser.add_argument('fname', nargs='?', default=None, help="Nom du fichier à afficher") - parsed = parser.parse_args(sys.argv[1:]) - SERVER = config.servers[parsed.server] - QUIET = parsed.quiet - VERB = parsed.verbose and not QUIET - if parsed.clipboard != None: - CLIPBOARD = parsed.clipboard - FORCED = parsed.force - NEWROLES = parse_roles(parsed.roles) + # On parse les options fournies en commandline + options = parser.parse_args(sys.argv[1:]) - if NEWROLES != False: - if parsed.action.func_code.co_argcount == 0: - parsed.action() - elif parsed.fname == None: - if not QUIET: - print(u"Vous devez fournir un nom de fichier avec cette commande".encode("utf-8")) - parser.print_help() - sys.exit(1) - else: - parsed.action(parsed.fname) + ## On calcule les options qui dépendent des autres. + ## C'est un peu un hack, peut-être que la méthode propre serait de surcharger argparse.ArgumentParser + ## et argparse.Namespace, mais j'ai pas réussi à comprendre commenr m'en sortir. + # On utilise le clipboard si on n'a pas demandé explicitement à ne pas le faire, + # qu'on n'est pas en session distante et qu'on a xclip + options.clipboard = not options.noclipboard and bool(os.getenv('DISPLAY')) and os.path.exists('/usr/bin/xclip') + # On récupère les données du serveur à partir du nom fourni + options.serverdata = config.servers[options.server] + # Attention à l'ordre pour interactive + # --quiet override --verbose + if options.quiet: + options.verbose = False + # On parse les roles fournis, et il doivent exister, ne pas être -w… + # parse_roles s'occupe de ça + options.roles = parse_roles(options) - saveclipboard(restore=True) - + # Si l'utilisateur a demandé une action qui nécessite un nom de fichier, + # on vérifie qu'il a bien fourni un nom de fichier. + if options.action in NEED_FILENAME: + insult_on_nofilename(options, parser) + + # On exécute l'action demandée + options.action(options) -- GitLab