From 3280a48260f5f7ea509d27ad7a2678662980ea9f Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 10:02:44 +0200 Subject: [PATCH 01/19] Use paramiko for SSH --- README | 3 ++ client.py | 56 ++++++++++++++++------------- clientconfig.example.py | 12 ++++--- clientconfigs/crans/clientconfig.py | 19 +++++----- clientconfigs/tudor/clientconfig.py | 15 ++++---- 5 files changed, 61 insertions(+), 44 deletions(-) diff --git a/README b/README index 4ef3794..b81b26b 100644 --- a/README +++ b/README @@ -9,6 +9,9 @@ c'est possible. Il faut pour cela changer la variable cmd_name dans le Makefile avant de lancer make install ou make install-server. == Installation et configuration du client == + +Sous Debian il faut avoir `python2.7` et `python-paramiko` d'installés. + * Copiez le dépôt git sur votre machine : $ git clone git@gitlab.crans.org:nounous/cranspasswords.git * Si ce n'est déjà fait, indiquer votre clé publique sur gest_crans diff --git a/client.py b/client.py index 8d00756..76322ed 100755 --- a/client.py +++ b/client.py @@ -25,6 +25,9 @@ import time import datetime import copy +# Import a SSH client +from paramiko.client import SSHClient + # Import de la config envvar = "CRANSPASSWORDS_CLIENT_CONFIG_DIR" try: @@ -265,35 +268,38 @@ class simple_memoize(object): def remote_proc(options, command, arg=None): """ Fabrique un process distant pour communiquer avec le serveur. - Cela consiste à lancer une commande (indiquée dans la config) - qui va potentiellement lancer ssh. + Cela consiste à lancer une commande par ssh (indiquée dans la config). ``command`` désigne l'action à envoyer au serveur ``arg`` est une chaîne (str) accompagnant la commande en paramètre ``options`` contient la liste usuelle d'options """ - full_command = list(options.serverdata['server_cmd']) - full_command.append(command) + host = str(options.serverdata['host']) + remote_cmd = list(options.serverdata['remote_cmd']) + remote_cmd.append(command) if arg: - full_command.append(arg) + remote_cmd.append(arg) if options.verbose and not options.quiet: - print("Running command %s ..." % " ".join(full_command)) + print("Running command %s ..." % " ".join(remote_cmd)) - proc = subprocess.Popen(full_command, - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = sys.stderr, - close_fds = True) - return proc + # Crée un client SSH et charge l'agent et les clés du système + client = SSHClient() + client.load_system_host_keys() + client.connect(host) + + # Crée un Channel et lance la commande + chan = client.get_transport().open_session() + chan.exec_command(remote_cmd) + return chan @simple_memoize def get_keep_alive_connection(options): """Fabrique un process parlant avec le serveur suivant la commande 'keep-alive'. On utilise une fonction séparée pour cela afin de memoizer le résultat, et ainsi utiliser une seule connexion""" - proc = remote_proc(options, 'keep-alive', None) - atexit.register(proc.stdin.close) - return proc + chan = remote_proc(options, 'keep-alive', None) + atexit.register(chan.close) + return chan def remote_command(options, command, arg=None, stdin_contents=None): """Exécute la commande distante, et retourne la sortie de cette @@ -302,20 +308,22 @@ def remote_command(options, command, arg=None, stdin_contents=None): keep_alive = options.serverdata.get('keep-alive', False) if keep_alive: - conn = get_keep_alive_connection(options) + chan = get_keep_alive_connection(options) args = filter(None, [arg, stdin_contents]) msg = {u'action': unicode(command), u'args': args } - conn.stdin.write('%s\n' % json.dumps(msg)) - conn.stdin.flush() - raw_out = conn.stdout.readline() + stdin = chan.makefile_stdin() + stdin.write('%s\n' % json.dumps(msg)) + stdin.flush() + raw_out = chan.recv() else: - proc = remote_proc(options, command, arg) + chan = remote_proc(options, command, arg) if stdin_contents is not None: - proc.stdin.write(json.dumps(stdin_contents)) - proc.stdin.flush() + stdin = chan.makefile_stdin() + stdin.write(json.dumps(stdin_contents)) + stdin.flush() - raw_out, raw_err = proc.communicate() - ret = proc.returncode + raw_out = chan.recv() + ret = chan.recv_exit_status() if ret != 0: if not options.quiet: diff --git a/clientconfig.example.py b/clientconfig.example.py index ccdc5a2..13ba000 100755 --- a/clientconfig.example.py +++ b/clientconfig.example.py @@ -21,18 +21,20 @@ distant_cmd = ["sudo", '-n', server_path] #: #: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. #: -#: * ``'server_cmd'`` : La commande exécutée sur le client pour appeler -#: le script sur le serveur distant. +#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. servers = { 'default': { - 'server_cmd': [ssh_path, 'odlyd.crans.org'] + distant_cmd, + 'host': 'odlyd.crans.org', + 'remote_cmd': distant_cmd, }, # Utile pour tester 'localhost': { - 'server_cmd': [ssh_path, 'localhost'] + distant_cmd, + 'host': 'localhost', + 'remote_cmd': distant_cmd, 'keep-alive': True, # <-- experimental, n'ouvre qu'une connexion }, 'ovh': { - 'server_cmd': [ssh_path, 'ovh.crans.org'] + distant_cmd, + 'host': 'soyouz.crans.org', + 'remote_cmd': distant_cmd, } } diff --git a/clientconfigs/crans/clientconfig.py b/clientconfigs/crans/clientconfig.py index b3cd623..2ccc131 100644 --- a/clientconfigs/crans/clientconfig.py +++ b/clientconfigs/crans/clientconfig.py @@ -8,9 +8,6 @@ import os #: Pour override le nom si vous voulez renommer la commande cmd_name = 'cranspasswords' -#: Path du binaire ssh sur la machine client -ssh_path = '/usr/bin/ssh' - #: Path du script ``cmd_name``-server sur le serveur server_path = '/usr/local/bin/%s-server' % (cmd_name,) @@ -22,21 +19,25 @@ print distant_cmd #: #: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. #: -#: * ``'server_cmd'`` : La commande exécutée sur le client pour appeler -#: le script sur le serveur distant. +#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. servers = { 'default': { - 'server_cmd': [ssh_path, 'odlyd.crans.org'] + distant_cmd, + 'host': 'odlyd.crans.org', + 'remote_cmd': distant_cmd, }, 'titanic': { - 'server_cmd': [ssh_path, 'freebox.crans.org', ssh_path, 'odlyd.crans.org'] + distant_cmd, + 'host': 'freebox.crans.org', + # manual ssh jump + 'remote_cmd': ['ssh', 'odyld.crans.org'] + distant_cmd, }, # Utile pour tester 'localhost': { - 'server_cmd': [ssh_path, 'localhost'] + distant_cmd, + 'host': 'localhost', + 'remote_cmd': distant_cmd, 'keep-alive': True, # <-- experimental, n'ouvre qu'une connexion }, 'ovh': { - 'server_cmd': [ssh_path, 'soyouz.crans.org', 'sudo', '-n', '/usr/local/bin/cpasswords-server'], + 'host': 'soyouz.crans.org', + 'remote_cmd': ['sudo', '-n', '/usr/local/bin/cpasswords-server'], } } diff --git a/clientconfigs/tudor/clientconfig.py b/clientconfigs/tudor/clientconfig.py index eac5e68..9982694 100644 --- a/clientconfigs/tudor/clientconfig.py +++ b/clientconfigs/tudor/clientconfig.py @@ -10,23 +10,26 @@ import os #: #: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. #: -#: * ``'server_cmd'`` : La commande exécutée sur le client pour appeler -#: le script sur le serveur distant. +#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. servers = { 'default': { - 'server_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], + 'host': '', + 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], 'keep-alive': True, }, 'gladys': { - 'server_cmd': ['/usr/bin/ssh', 'home.b2moo.fr', '/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], + 'host': 'home.b2moo.fr', + 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], 'keep-alive': True, }, 'gladys-home': { - 'server_cmd': ['/usr/bin/ssh', 'gladys.home', '/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], + 'host': 'gladys.home', + 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], 'keep-alive': True, }, 'pimeys': { - 'server_cmd': ['/usr/bin/ssh', 'pimeys.fr', 'sudo', '-n', '/usr/local/bin/cranspasswords-server', ], + 'host': 'pimeys.fr', + 'remote_cmd': ['sudo', '-n', '/usr/local/bin/cranspasswords-server', ], 'keep-alive': True, }, } -- GitLab From 91ddc03cadf003786c76bccc311f6124de2a02ca Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 10:45:08 +0200 Subject: [PATCH 02/19] Use Python logging module --- client.py | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/client.py b/client.py index 76322ed..7d7c42a 100755 --- a/client.py +++ b/client.py @@ -24,15 +24,19 @@ import string import time import datetime import copy +import logging # Import a SSH client from paramiko.client import SSHClient +# Logger local +# On n'a pas encore accès à la config donc on devine le nom +bootstrap_cmd_name = os.path.split(sys.argv[0])[1] +log = logging.getLogger(bootstrap_cmd_name) + # Import de la config envvar = "CRANSPASSWORDS_CLIENT_CONFIG_DIR" try: - # Oui, le nom de la commande est dans la config, mais on n'a pas encore accès à la config - bootstrap_cmd_name = os.path.split(sys.argv[0])[1] sys.path.append(os.path.expanduser("~/.config/%s/" % (bootstrap_cmd_name,))) import clientconfig as config except ImportError: @@ -975,16 +979,27 @@ def insult_on_nofilename(options, parser): sys.exit(1) if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Gestion de mots de passe partagés grâce à GPG.") + # Gestion des arguments + parser = argparse.ArgumentParser( + description="Gestion de mots de passe partagés grâce à GPG.", + ) + parser.add_argument( + '--verbose', '-v', + action='count', + default=1, + help="Verbose mode. Multiple -v options increase the verbosity.", + ) + parser.add_argument( + '--quiet', '-q', + action='store_true', + default=False, + help="Mode silencieux. Cache les message d'erreurs (override --verbose et --interactive)." + ) 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 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, @@ -1048,6 +1063,18 @@ if __name__ == "__main__": # On parse les options fournies en commandline options = parser.parse_args(sys.argv[1:]) + # Active le logger avec des couleurs + # Par défaut on affiche >= WARNING + options.verbose = 40 - (10 * options.verbose) if not options.quiet else 0 + logging.addLevelName(logging.INFO, "\033[1;36mINFO\033[1;0m") + logging.addLevelName(logging.WARNING, "\033[1;33mWARNING\033[1;0m") + logging.addLevelName(logging.ERROR, "\033[1;91mERROR\033[1;0m") + logging.addLevelName(logging.DEBUG, "\033[1;37mDEBUG\033[1;0m") + logging.basicConfig( + level=options.verbose, + format='\033[1;90m%(asctime)s\033[1;0m %(name)s %(levelname)s %(message)s' + ) + ## 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. @@ -1059,10 +1086,6 @@ if __name__ == "__main__": options.clipboard = 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 # NB : ça nécessite de se connecter au serveur, or, pour show_servers on n'en a pas besoin -- GitLab From e13d7564e2e447f591463334db1c43cb768939ce Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 12:20:04 +0200 Subject: [PATCH 03/19] Split Paramiko client and channels code --- client.py | 133 ++++++++++++++++++++++-------------------------------- 1 file changed, 55 insertions(+), 78 deletions(-) diff --git a/client.py b/client.py index 7d7c42a..c649236 100755 --- a/client.py +++ b/client.py @@ -25,9 +25,10 @@ import time import datetime import copy import logging +from binascii import hexlify # Import a SSH client -from paramiko.client import SSHClient +from paramiko.client import SSHClient, MissingHostKeyPolicy # Logger local # On n'a pas encore accès à la config donc on devine le nom @@ -269,97 +270,73 @@ class simple_memoize(object): ###### ## Remote commands -def remote_proc(options, command, arg=None): +class MyMissingHostKeyPolicy(MissingHostKeyPolicy): """ - Fabrique un process distant pour communiquer avec le serveur. - Cela consiste à lancer une commande par ssh (indiquée dans la config). - ``command`` désigne l'action à envoyer au serveur - ``arg`` est une chaîne (str) accompagnant la commande en paramètre - ``options`` contient la liste usuelle d'options + Warn when hostkey is unknown and then add """ - host = str(options.serverdata['host']) - remote_cmd = list(options.serverdata['remote_cmd']) - remote_cmd.append(command) - if arg: - remote_cmd.append(arg) - - if options.verbose and not options.quiet: - print("Running command %s ..." % " ".join(remote_cmd)) + def missing_host_key(self, client, hostname, key): + client._host_keys.add(hostname, key.get_name(), key) + if client._host_keys_filename is not None: + client.save_host_keys(client._host_keys_filename) + log.warning('Adding %s host key for %s: %s' % + (key.get_name(), hostname, hexlify(key.get_fingerprint()))) + +def create_ssh_client(): + """ + Create a SSH client with paramiko module + """ + if not "host" in options.serverdata: + log.error("Missing parameter `host` in active server configuration") + exit(1) - # Crée un client SSH et charge l'agent et les clés du système + # Create SSH client with system host keys and agent client = SSHClient() client.load_system_host_keys() - client.connect(host) + client.set_missing_host_key_policy(MyMissingHostKeyPolicy()) + client.connect(str(options.serverdata['host'])) + return client - # Crée un Channel et lance la commande - chan = client.get_transport().open_session() - chan.exec_command(remote_cmd) - return chan +def remote_command(options, command, arg=None, stdin_contents=None): + """ + Execute remote command and return output + """ + # TODO: instantiate client outside to have only one connection + client = create_ssh_client() + + # Build command + if not "remote_cmd" in options.serverdata: + log.error("Missing parameter `remote_cmd` in active server configuration") + exit(1) + remote_cmd = " ".join(options.serverdata['remote_cmd'] + [command]) + if arg: + remote_cmd += " " + arg -@simple_memoize -def get_keep_alive_connection(options): - """Fabrique un process parlant avec le serveur suivant la commande - 'keep-alive'. On utilise une fonction séparée pour cela afin - de memoizer le résultat, et ainsi utiliser une seule connexion""" - chan = remote_proc(options, 'keep-alive', None) - atexit.register(chan.close) - return chan + # Run command + log.info("Running command %s ..." % remote_cmd) + stdin, stdout, stderr = client.exec_command(remote_cmd) -def remote_command(options, command, arg=None, stdin_contents=None): - """Exécute la commande distante, et retourne la sortie de cette - commande""" - detail = options.verbose and not options.quiet - keep_alive = options.serverdata.get('keep-alive', False) - - if keep_alive: - chan = get_keep_alive_connection(options) - args = filter(None, [arg, stdin_contents]) - msg = {u'action': unicode(command), u'args': args } - stdin = chan.makefile_stdin() - stdin.write('%s\n' % json.dumps(msg)) + # Write + if stdin_contents is not None: + stdin.write(json.dumps(stdin_contents)) stdin.flush() - raw_out = chan.recv() - else: - chan = remote_proc(options, command, arg) - if stdin_contents is not None: - stdin = chan.makefile_stdin() - stdin.write(json.dumps(stdin_contents)) - stdin.flush() - raw_out = chan.recv() - ret = chan.recv_exit_status() + # Read + raw_out = stdout.read() + log.debug("Server returned %s" % raw_out) + + # Return code == 0 if success + ret = stdout.channel.recv_exit_status() + if ret != 0: + log.error("Wrong server return code %s, error is %s" % (ret, stderr.read())) + exit(ret) - if ret != 0: - if not options.quiet: - print((u"Mauvais code retour côté serveur, voir erreur " + - u"ci-dessus").encode('utf-8'), - file=sys.stderr) - if detail: - print("raw_output: %s" % raw_out) - sys.exit(ret) try: answer = json.loads(raw_out.strip()) except ValueError: - if not options.quiet: - print(u"Impossible de parser le résultat".encode('utf-8'), - file=sys.stderr) - if detail: - print("raw_output: %s" % raw_out) - sys.exit(42) - if not keep_alive: - return answer - else: - try: - if answer[u'status'] != u'ok': - raise KeyError('Bad answer status') - return answer[u'content'] - except KeyError: - if not options.quiet: - print(u"Réponse erronée du serveur".encode('utf-8'), - file=sys.stderr) - if detail: - print("answer: %s" % repr(answer)) - sys.exit(-1) + log.error("Error while parsing JSON") + exit(42) + + return answer @simple_memoize -- GitLab From 5537150c90aab06848b940a595f9ae8add806429 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 13:19:48 +0200 Subject: [PATCH 04/19] Check return code before reading --- client.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/client.py b/client.py index c649236..7577d09 100755 --- a/client.py +++ b/client.py @@ -311,31 +311,33 @@ def remote_command(options, command, arg=None, stdin_contents=None): if arg: remote_cmd += " " + arg - # Run command - log.info("Running command %s ..." % remote_cmd) - stdin, stdout, stderr = client.exec_command(remote_cmd) + # Run command and timeout after 10s + log.info("Running command `%s`" % remote_cmd) + stdin, stdout, stderr = client.exec_command(remote_cmd, timeout=10) # Write if stdin_contents is not None: + log.info("Writing to stdin: %s" % stdin_contents) stdin.write(json.dumps(stdin_contents)) stdin.flush() - # Read - raw_out = stdout.read() - log.debug("Server returned %s" % raw_out) - # Return code == 0 if success ret = stdout.channel.recv_exit_status() if ret != 0: - log.error("Wrong server return code %s, error is %s" % (ret, stderr.read())) + err = "" + if sterr.channel.recv_stderr_ready(): + err = stderr.read() + log.error("Wrong server return code %s, error is %s" % (ret, err)) exit(ret) + # Decode directly read buffer try: - answer = json.loads(raw_out.strip()) + answer = json.load(stdout) except ValueError: log.error("Error while parsing JSON") exit(42) + log.debug("Server returned %s" % answer) return answer @@ -961,16 +963,16 @@ if __name__ == "__main__": description="Gestion de mots de passe partagés grâce à GPG.", ) parser.add_argument( - '--verbose', '-v', + '-v', '--verbose', action='count', default=1, - help="Verbose mode. Multiple -v options increase the verbosity.", + help="verbose mode, multiple -v options increase verbosity", ) parser.add_argument( - '--quiet', '-q', + '-q', '--quiet', action='store_true', default=False, - help="Mode silencieux. Cache les message d'erreurs (override --verbose et --interactive)." + help="silent mode: hide errors, overrides verbosity" ) parser.add_argument('-s', '--server', default='default', help="Utilisation d'un serveur alternatif (test, backup, etc)") @@ -1038,7 +1040,7 @@ if __name__ == "__main__": help="Nom du fichier à afficher") # On parse les options fournies en commandline - options = parser.parse_args(sys.argv[1:]) + options = parser.parse_args() # Active le logger avec des couleurs # Par défaut on affiche >= WARNING -- GitLab From ba517c0b04d9ec5b9f549922748c07ce4be9cc6b Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 13:26:59 +0200 Subject: [PATCH 05/19] Get one file at a time --- client.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/client.py b/client.py index 7577d09..ea69435 100755 --- a/client.py +++ b/client.py @@ -361,10 +361,12 @@ def restore_all_files(options): """Récupère les fichiers du serveur distant""" return remote_command(options, "restorefiles") - -def get_files(options, filenames): - """Récupère le contenu des fichiers distants""" - return remote_command(options, "getfiles", stdin_contents=filenames) +@simple_memoize +def get_file(options, filename): + """ + Get the content of one remote file + """ + return remote_command(options, "getfile", filename) def put_files(options, files): """Dépose les fichiers sur le serveur distant""" @@ -654,7 +656,7 @@ def clipboard(texte): def show_file(options): """Affiche le contenu d'un fichier""" fname = options.fname - gotit, value = get_files(options, [fname])[0] + gotit, value = get_file(options, fname) if not gotit: if not options.quiet: print(value.encode("utf-8")) # value contient le message d'erreur @@ -736,7 +738,7 @@ def show_file(options): def edit_file(options): """Modifie/Crée un fichier""" fname = options.fname - gotit, value = get_files(options, [fname])[0] + gotit, value = get_file(options, fname) nfile = False annotations = u"" @@ -882,7 +884,8 @@ def recrypt_files(options, strict=False): askfiles = [filename for (filename, fileroles) in allfiles.iteritems() if is_wanted(fileroles) ] - files = get_files(options, askfiles) + files = [get_file(options, f) for f in 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: -- GitLab From fa7883e16fd9da22cb8aea9d79a87ff678fe2d67 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 13:35:04 +0200 Subject: [PATCH 06/19] Memoize SSHClient --- client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client.py b/client.py index ea69435..859a319 100755 --- a/client.py +++ b/client.py @@ -47,7 +47,7 @@ except ImportError: envspecified = os.getenv(envvar, None) if envspecified is None: if ducktape_display_error: - sys.stderr.write(u"Va lire le fichier README.\n".encode("utf-8")) + log.error("Va lire le fichier README.") sys.exit(1) else: # On a spécifié à la main le dossier de conf @@ -281,6 +281,7 @@ class MyMissingHostKeyPolicy(MissingHostKeyPolicy): log.warning('Adding %s host key for %s: %s' % (key.get_name(), hostname, hexlify(key.get_fingerprint()))) +@simple_memoize def create_ssh_client(): """ Create a SSH client with paramiko module -- GitLab From 3aa59583778a4fcb88128894b3c13e0a2387f162 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 13:37:27 +0200 Subject: [PATCH 07/19] Update default config --- client.py | 4 ++-- clientconfig.example.py | 1 - clientconfigs/crans/clientconfig.py | 1 - clientconfigs/tudor/clientconfig.py | 4 ---- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/client.py b/client.py index 859a319..efe3015 100755 --- a/client.py +++ b/client.py @@ -259,8 +259,8 @@ class simple_memoize(object): mais il faudra s'en préoccuper si un jour on veut changer le comportement.""" if self.val == None: self.val = self.f(*args, **kwargs) - # On évite de tout deepcopier. Typiquement, un subprocess.Popen - # ne devrait pas l'être (comme dans get_keep_alive_connection) + # On évite de tout deepcopier. Typiquement, un SSHClient + # ne devrait pas l'être (comme dans create_ssh_client) if type(self.val) in [dict, list]: return copy.deepcopy(self.val) else: diff --git a/clientconfig.example.py b/clientconfig.example.py index 13ba000..78b061f 100755 --- a/clientconfig.example.py +++ b/clientconfig.example.py @@ -31,7 +31,6 @@ servers = { 'localhost': { 'host': 'localhost', 'remote_cmd': distant_cmd, - 'keep-alive': True, # <-- experimental, n'ouvre qu'une connexion }, 'ovh': { 'host': 'soyouz.crans.org', diff --git a/clientconfigs/crans/clientconfig.py b/clientconfigs/crans/clientconfig.py index 2ccc131..1e22826 100644 --- a/clientconfigs/crans/clientconfig.py +++ b/clientconfigs/crans/clientconfig.py @@ -34,7 +34,6 @@ servers = { 'localhost': { 'host': 'localhost', 'remote_cmd': distant_cmd, - 'keep-alive': True, # <-- experimental, n'ouvre qu'une connexion }, 'ovh': { 'host': 'soyouz.crans.org', diff --git a/clientconfigs/tudor/clientconfig.py b/clientconfigs/tudor/clientconfig.py index 9982694..3f3e688 100644 --- a/clientconfigs/tudor/clientconfig.py +++ b/clientconfigs/tudor/clientconfig.py @@ -15,21 +15,17 @@ servers = { 'default': { 'host': '', 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - 'keep-alive': True, }, 'gladys': { 'host': 'home.b2moo.fr', 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - 'keep-alive': True, }, 'gladys-home': { 'host': 'gladys.home', 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - 'keep-alive': True, }, 'pimeys': { 'host': 'pimeys.fr', 'remote_cmd': ['sudo', '-n', '/usr/local/bin/cranspasswords-server', ], - 'keep-alive': True, }, } -- GitLab From 0d847ba2a3e63f00661ffcb31b8b85a7a180e386 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 13:46:42 +0200 Subject: [PATCH 08/19] Use logger for all errors --- client.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/client.py b/client.py index efe3015..5578328 100755 --- a/client.py +++ b/client.py @@ -48,7 +48,7 @@ except ImportError: if envspecified is None: if ducktape_display_error: log.error("Va lire le fichier README.") - sys.exit(1) + exit(1) else: # On a spécifié à la main le dossier de conf try: @@ -56,8 +56,8 @@ except ImportError: import clientconfig as config except ImportError: if ducktape_display_error: - sys.stderr.write(u"%s est spécifiée, mais aucune config pour le client ne peut être importée." % (envvar)) - sys.exit(1) + log.error("%s is passed, but no config could be imported" % envvar) + exit(1) #: 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?$', @@ -899,7 +899,7 @@ def recrypt_files(options, strict=False): 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) + exit(2) # On rechiffre to_put = [{'filename' : f['filename'], 'roles' : f['roles'], @@ -942,24 +942,21 @@ def parse_roles(options, cast=False): ret = set() for role in strroles.split(','): 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) + log.warn("role %s do not exists" % role) + exit(1) if role.endswith('-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) + log.warn("role %s should not be used, rather use %s" % (role, role[:-2])) + exit(1) ret.add(role) 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: + log.warn("You need to provide a filename with this command") 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) + exit(1) if __name__ == "__main__": # Gestion des arguments @@ -1055,7 +1052,7 @@ if __name__ == "__main__": logging.addLevelName(logging.DEBUG, "\033[1;37mDEBUG\033[1;0m") logging.basicConfig( level=options.verbose, - format='\033[1;90m%(asctime)s\033[1;0m %(name)s %(levelname)s %(message)s' + format='\033[90m%(asctime)s\033[1;0m %(name)s %(levelname)s %(message)s' ) ## On calcule les options qui dépendent des autres. -- GitLab From 2a0cec2ed1becf7b0f5a5d10b95c01e5dd2e0d09 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 14:04:56 +0200 Subject: [PATCH 09/19] Use logger for more warnings --- client.py | 49 +++++++++++++++++++------------------------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/client.py b/client.py index 5578328..d58b5ee 100755 --- a/client.py +++ b/client.py @@ -102,8 +102,9 @@ def gpg(options, command, args=None): else: stderr = subprocess.PIPE full_command.extend(['--debug-level=1']) - if options.verbose: - print(" ".join(full_command)) + + # Run gpg + log.info("Running `%s`" % " ".join(full_command)) proc = subprocess.Popen(full_command, stdin = subprocess.PIPE, stdout = subprocess.PIPE, @@ -171,10 +172,6 @@ GPG_PARSERS = { u'sub' : _parse_sub, } -def _gpg_printdebug(d): - print("current_pub : %r" % d.get("current_pub", None)) - print("current_sub : %r" % d.get("current_sub", None)) - def parse_keys(gpgout, debug=False): """Parse l'output d'un listing de clés gpg.""" ring = {} @@ -190,19 +187,17 @@ def parse_keys(gpgout, debug=False): line = line.decode("iso8859-1") except UnicodeDecodeError: line = line.decode("iso8859-1", "ignore") - if not options.quiet: - print("gpg raconte de la merde, c'est ni de l'ISO-8859-1 ni de l'UTF-8, je droppe, ça risque de foirer plus tard.", sys.stderr) + log.warning("gpg is telling shit, it is neither ISO-8859-1 nor UTF-8. Dropping!") line = line.split(":") field = line[0] if field in GPG_PARSERS.keys(): - if debug: - print("\nbegin loop. met %s :" % (field)) - _gpg_printdebug(locals()) + log.debug("begin loop, met %s :" % (field)) + log.debug("current_pub : %r" % current_pub) + log.debug("current_sub : %r" % current_sub) try: content = GPG_PARSERS[field](line) except: - print("*** FAILED ***") - print(line) + log.error("*** FAILED *** Line: %s", line) raise if field == u"pub": # Nouvelle clé @@ -235,9 +230,9 @@ def parse_keys(gpgout, debug=False): current_pub[u"subkeys"].append(current_sub) # On place la nouvelle comme sub courant current_sub = content - if debug: - _gpg_printdebug(locals()) - print("parsed object : %r" % content) + log.debug("current_pub : %r" % current_pub) + log.debug("current_sub : %r" % current_sub) + log.debug("parsed object : %r" % content) # À la fin, il faut sauvegarder les derniers (sub, pub) rencontrés, # parce que leur sauvegarde n'a pas encore été déclenchée if current_sub != init_value: @@ -470,8 +465,7 @@ def check_keys(options, recipients=None, quiet=False): if speak: print("") if failed: - if not options.quiet: - print((u"--> Fail on %s:%s\n--> %s" % (mail, fpr, failed)).encode("utf-8")) + log.warn("--> Fail on %s:%s\n--> %s" % (mail, fpr, failed)) if not recipients is None: # On cherche à savoir si on droppe ce recipient message = u"Abandonner le chiffrement pour cette clé ? (Si vous la conservez, il est posible que gpg crashe)" @@ -484,8 +478,7 @@ def check_keys(options, recipients=None, quiet=False): if not drop: trusted_recipients.append(recipient) else: - if not options.quiet: - print(u"Droppe la clé %s:%s" % (fpr, recipient)) + log.warn("Droppe la clé %s:%s" % (fpr, recipient)) else: trusted_recipients.append(recipient) if recipients is None: @@ -536,7 +529,7 @@ def decrypt(options, contents): """Déchiffre le contenu""" stdin, stdout = gpg(options, "decrypt") if type(contents) != unicode: # Kludge (broken db ?) - print("Eau dans le gaz (decrypt)" + repr(contents)) + log.warn("Eau dans le gaz (decrypt)" + repr(contents)) contents = contents[-1] stdin.write(contents.encode("utf-8")) stdin.close() @@ -659,8 +652,7 @@ def show_file(options): fname = options.fname gotit, value = get_file(options, fname) if not gotit: - if not options.quiet: - print(value.encode("utf-8")) # value contient le message d'erreur + log.warn(value) # value contient le message d'erreur return passfile = value (sin, sout) = gpg(options, 'decrypt') @@ -668,7 +660,7 @@ def show_file(options): # Kludge (broken db ?) if type(content) == list: - print("Eau dans le gaz") + log.warn("Eau dans le gaz") content = content[-1] # Déchiffre le contenu @@ -762,8 +754,7 @@ Enregistrez le fichier vide pour annuler.\n""" new_roles = parse_roles(options, cast=True) passfile = {'roles' : new_roles} elif not gotit: - if not options.quiet: - print(value.encode("utf-8")) # value contient le message d'erreur + log.warn(value) return else: passfile = value @@ -782,8 +773,7 @@ Enregistrez le fichier vide pour annuler.\n""" # On vérifie qu'on a le droit actuellement (plutôt que de se faire jeter # plus tard) if not any(r + '-w' in my_roles for r in passfile['roles']): - if not options.quiet: - print(u"Aucun rôle en écriture pour ce fichier ! Abandon.".encode("utf-8")) + log.warn("Aucun rôle en écriture pour ce fichier ! Abandon.") return # On peut vouloir chiffrer un fichier sans avoir la possibilité de le lire @@ -891,8 +881,7 @@ def recrypt_files(options, strict=False): # on affiche le message d'erreur correspondant et on abandonne. for (success, message) in files: if not success: - if not options.quiet: - print(message.encode("utf-8")) + log.warn(message) return # On informe l'utilisateur et on demande confirmation avant de rechiffrer # Si il a précisé --force, on ne lui demandera rien. -- GitLab From ac3b5a98ea0132b56f7f9cbdac63726a6cea2a30 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 14:22:40 +0200 Subject: [PATCH 10/19] Warn on outdated paramiko --- client.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/client.py b/client.py index d58b5ee..b8eba53 100755 --- a/client.py +++ b/client.py @@ -28,7 +28,8 @@ import logging from binascii import hexlify # Import a SSH client -from paramiko.client import SSHClient, MissingHostKeyPolicy +from paramiko.client import SSHClient +from paramiko.ssh_exception import SSHException # Logger local # On n'a pas encore accès à la config donc on devine le nom @@ -265,17 +266,6 @@ class simple_memoize(object): ###### ## Remote commands -class MyMissingHostKeyPolicy(MissingHostKeyPolicy): - """ - Warn when hostkey is unknown and then add - """ - def missing_host_key(self, client, hostname, key): - client._host_keys.add(hostname, key.get_name(), key) - if client._host_keys_filename is not None: - client.save_host_keys(client._host_keys_filename) - log.warning('Adding %s host key for %s: %s' % - (key.get_name(), hostname, hexlify(key.get_fingerprint()))) - @simple_memoize def create_ssh_client(): """ @@ -288,8 +278,12 @@ def create_ssh_client(): # Create SSH client with system host keys and agent client = SSHClient() client.load_system_host_keys() - client.set_missing_host_key_policy(MyMissingHostKeyPolicy()) - client.connect(str(options.serverdata['host'])) + try: + client.connect(str(options.serverdata['host'])) + except SSHException: + log.error("Host key is unknown or you are using a outdated python-paramiko (ssh-ed25519 was implemented in 2017)") + raise + return client def remote_command(options, command, arg=None, stdin_contents=None): @@ -584,11 +578,11 @@ def editor(texte, annotations=u""): 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(options) files = all_files(options) keys = files.keys() keys.sort() + print(u"Liste des fichiers disponibles :".encode("utf-8")) for fname in keys: froles = files[fname] access = set(my_roles).intersection(froles) != set([]) -- GitLab From 61dbaacdedcb2b41c3430fd10f168f3a70c33e71 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 16:10:50 +0200 Subject: [PATCH 11/19] Switch to INI configuration format --- client.py | 46 +++++++++++----------------- clientconfig.example.ini | 17 ++++++++++ clientconfig.example.py | 39 ----------------------- clientconfigs/crans/clientconfig.ini | 21 +++++++++++++ clientconfigs/crans/clientconfig.py | 42 ------------------------- clientconfigs/tudor/clientconfig.ini | 21 +++++++++++++ clientconfigs/tudor/clientconfig.py | 31 ------------------- 7 files changed, 77 insertions(+), 140 deletions(-) create mode 100755 clientconfig.example.ini delete mode 100755 clientconfig.example.py create mode 100644 clientconfigs/crans/clientconfig.ini delete mode 100644 clientconfigs/crans/clientconfig.py create mode 100644 clientconfigs/tudor/clientconfig.ini delete mode 100644 clientconfigs/tudor/clientconfig.py diff --git a/client.py b/client.py index b8eba53..354d530 100755 --- a/client.py +++ b/client.py @@ -26,39 +26,30 @@ import datetime import copy import logging from binascii import hexlify +from configparser import ConfigParser # Import a SSH client from paramiko.client import SSHClient from paramiko.ssh_exception import SSHException -# Logger local +# Configuration loading # On n'a pas encore accès à la config donc on devine le nom bootstrap_cmd_name = os.path.split(sys.argv[0])[1] -log = logging.getLogger(bootstrap_cmd_name) - -# Import de la config -envvar = "CRANSPASSWORDS_CLIENT_CONFIG_DIR" -try: - sys.path.append(os.path.expanduser("~/.config/%s/" % (bootstrap_cmd_name,))) - import clientconfig as config -except ImportError: +default_config_path = os.path.expanduser("~/.config/" + bootstrap_cmd_name) +config_path = os.getenv("CRANSPASSWORDS_CLIENT_CONFIG_DIR", default_config_path) +config = ConfigParser() +if not config.read(config_path + "/clientconfig.ini"): + # If config could not be imported, display an error if required ducktape_display_error = sys.stderr.isatty() and \ not any([opt in sys.argv for opt in ["-q", "--quiet"]]) and \ __name__ == '__main__' - envspecified = os.getenv(envvar, None) - if envspecified is None: - if ducktape_display_error: - log.error("Va lire le fichier README.") - exit(1) - else: - # On a spécifié à la main le dossier de conf - try: - sys.path.append(envspecified) - import clientconfig as config - except ImportError: - if ducktape_display_error: - log.error("%s is passed, but no config could be imported" % envvar) - exit(1) + if ducktape_display_error: + # Do not use logger as it has not been initialized yet + print("%s/clientconfig.ini could not be read. Please read README." % config_path) + exit(1) + +# Logger local +log = logging.getLogger(bootstrap_cmd_name) #: 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?$', @@ -290,14 +281,13 @@ def remote_command(options, command, arg=None, stdin_contents=None): """ Execute remote command and return output """ - # TODO: instantiate client outside to have only one connection client = create_ssh_client() # Build command if not "remote_cmd" in options.serverdata: log.error("Missing parameter `remote_cmd` in active server configuration") exit(1) - remote_cmd = " ".join(options.serverdata['remote_cmd'] + [command]) + remote_cmd = options.serverdata['remote_cmd'] + " " + command if arg: remote_cmd += " " + arg @@ -612,7 +602,7 @@ def show_roles(options): 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(): + for server in config.keys(): print((u" * " + server).encode("utf-8")) def saveclipboard(restore=False, old_clipboard=None): @@ -958,7 +948,7 @@ if __name__ == "__main__": default=False, help="silent mode: hide errors, overrides verbosity" ) - parser.add_argument('-s', '--server', default='default', + parser.add_argument('-s', '--server', default='DEFAULT', help="Utilisation d'un serveur alternatif (test, backup, etc)") parser.add_argument('--drop-invalid', action='store_true', default=False, dest='drop_invalid', @@ -1048,7 +1038,7 @@ if __name__ == "__main__": if options.clipboard is None: options.clipboard = 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] + options.serverdata = config[options.server] # On parse les roles fournis, et il doivent exister, ne pas être -w… # parse_roles s'occupe de ça # NB : ça nécessite de se connecter au serveur, or, pour show_servers on n'en a pas besoin diff --git a/clientconfig.example.ini b/clientconfig.example.ini new file mode 100755 index 0000000..40c5aaa --- /dev/null +++ b/clientconfig.example.ini @@ -0,0 +1,17 @@ +#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. +#: +#: Sans précision du paramètre --server, la clé `DEFAULT` sera utilisée. +#: +#: `host` est l'ip ou le nom de domaine sur lequel se connecter +#: `remote_cmd' est la commande executée sur le serveur. +[DEFAULT] +host=odlyd.crans.org +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server + +[localhost] +host=localhost +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server + +[ovh] +host=soyouz.crans.org +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server diff --git a/clientconfig.example.py b/clientconfig.example.py deleted file mode 100755 index 78b061f..0000000 --- a/clientconfig.example.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python2 -# -*- encoding: utf-8 -*- - -""" Configuration du client cranspasswords """ - -import os - -#: Pour override le nom si vous voulez renommer la commande -cmd_name = 'cranspasswords' - -#: Path du binaire ssh sur la machine client -ssh_path = '/usr/bin/ssh' - -#: Path du script ``cmd_name``-server sur le serveur -server_path = '/usr/local/bin/%s-server' % (cmd_name,) - -#: Commande à exécuter sur le serveur après y être entré en ssh -distant_cmd = ["sudo", '-n', server_path] - -#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. -#: -#: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. -#: -#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. -servers = { - 'default': { - 'host': 'odlyd.crans.org', - 'remote_cmd': distant_cmd, - }, - # Utile pour tester - 'localhost': { - 'host': 'localhost', - 'remote_cmd': distant_cmd, - }, - 'ovh': { - 'host': 'soyouz.crans.org', - 'remote_cmd': distant_cmd, - } -} diff --git a/clientconfigs/crans/clientconfig.ini b/clientconfigs/crans/clientconfig.ini new file mode 100644 index 0000000..68d98cf --- /dev/null +++ b/clientconfigs/crans/clientconfig.ini @@ -0,0 +1,21 @@ +#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. +#: +#: Sans précision du paramètre --server, la clé `DEFAULT` sera utilisée. +#: +#: `host` est l'ip ou le nom de domaine sur lequel se connecter +#: `remote_cmd' est la commande executée sur le serveur. +[DEFAULT] +host=odlyd.crans.org +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server + +[titanic] +host=titanic +remote_cmd=ssh odlyd.crans.org sudo -n /usr/local/bin/cranspasswords-server + +[localhost] +host=localhost +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server + +[ovh] +host=soyouz.crans.org +remote_cmd=sudo -n /usr/local/bin/cpasswords-server diff --git a/clientconfigs/crans/clientconfig.py b/clientconfigs/crans/clientconfig.py deleted file mode 100644 index 1e22826..0000000 --- a/clientconfigs/crans/clientconfig.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python2 -# -*- encoding: utf-8 -*- - -""" Configuration du client cranspasswords """ - -import os - -#: Pour override le nom si vous voulez renommer la commande -cmd_name = 'cranspasswords' - -#: Path du script ``cmd_name``-server sur le serveur -server_path = '/usr/local/bin/%s-server' % (cmd_name,) - -#: Commande à exécuter sur le serveur après y être entré en ssh -distant_cmd = ["sudo", '-n', server_path] -print distant_cmd - -#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. -#: -#: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. -#: -#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. -servers = { - 'default': { - 'host': 'odlyd.crans.org', - 'remote_cmd': distant_cmd, - }, - 'titanic': { - 'host': 'freebox.crans.org', - # manual ssh jump - 'remote_cmd': ['ssh', 'odyld.crans.org'] + distant_cmd, - }, - # Utile pour tester - 'localhost': { - 'host': 'localhost', - 'remote_cmd': distant_cmd, - }, - 'ovh': { - 'host': 'soyouz.crans.org', - 'remote_cmd': ['sudo', '-n', '/usr/local/bin/cpasswords-server'], - } -} diff --git a/clientconfigs/tudor/clientconfig.ini b/clientconfigs/tudor/clientconfig.ini new file mode 100644 index 0000000..b553b44 --- /dev/null +++ b/clientconfigs/tudor/clientconfig.ini @@ -0,0 +1,21 @@ +#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. +#: +#: Sans précision du paramètre --server, la clé `DEFAULT` sera utilisée. +#: +#: `host` est l'ip ou le nom de domaine sur lequel se connecter +#: `remote_cmd' est la commande executée sur le serveur. +[DEFAULT] +host= +remote_cmd=/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server + +[gladys] +host=home.b2moo.fr +remote_cmd=/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server + +[gladys-home] +host=gladys.home +remote_cmd=/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server + +[pimeys] +host=pimeys.fr +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server diff --git a/clientconfigs/tudor/clientconfig.py b/clientconfigs/tudor/clientconfig.py deleted file mode 100644 index 3f3e688..0000000 --- a/clientconfigs/tudor/clientconfig.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python2 -# -*- encoding: utf-8 -*- - -""" Configuration du client cranspasswords """ - -import os - - -#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. -#: -#: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. -#: -#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. -servers = { - 'default': { - 'host': '', - 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - }, - 'gladys': { - 'host': 'home.b2moo.fr', - 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - }, - 'gladys-home': { - 'host': 'gladys.home', - 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - }, - 'pimeys': { - 'host': 'pimeys.fr', - 'remote_cmd': ['sudo', '-n', '/usr/local/bin/cranspasswords-server', ], - }, -} -- GitLab From d06ddf5b9cfaefdde9f84dbc017c026ff43378b5 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 16:23:48 +0200 Subject: [PATCH 12/19] Update CHANGELOG --- CHANGELOG | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 366ac4a..5252e24 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,17 @@ cranspasswords possède plusieurs branches. * 0.1, 0.2,… : anciennes versions (si vieux serveur), ça n'intègre plus de nouvelles fonctionnalités, seulement d'éventuels bugfix. +=== 0.2.0 === + +La configuration du client a changé de format, +il faut donc repartir du `clientconfig.example.ini`. + +Les nouveaux clients utilisent Paramiko pour réaliser la connexion SSH. +Cela permet de garder une unique session SSH ouverte. + +Le module logging de Python est maintenant utilisé. +Vous pouvez augmenter sa verbosité avec `-vv`. + === 0.1.5 === ''Pour voir cette version, git show 0.1.5'' -- GitLab From a96c6ef0ad0d108f9a962461f5f94dd601e74f06 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 20:24:03 +0200 Subject: [PATCH 13/19] Add Python3 continuous integration lint --- .gitignore | 24 +++++++++++++++++++++--- .gitlab-ci.yml | 14 ++++++++++++++ tox.ini | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 7f7305d..baee8a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,24 @@ +# Byte-compiled / optimized / DLL files +__pycache__ +*.py[cod] +*$py.class +*.swp +*.egg-info +_build + +# Virtualenvs +.tox +venv + +# Editors configuration +.ipynb_checkpoints +.spyderproject +.spyproject +.ropeproject +.idea +.vscode + +# Configuration files ./clientconfig.py ./serverconfig.py -*.pyc - -# Dossier contenant les mots de passe db diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..40929fa --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,14 @@ +image: python:3.7 + +stages: + - quality-assurance + +before_script: + - pip install tox + +linters: + stage: quality-assurance + script: tox -e linters + + # Be nice to new contributors, but please use `tox` before commit + allow_failure: true diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..277cfb4 --- /dev/null +++ b/tox.ini @@ -0,0 +1,39 @@ +[tox] +envlist = + linters +skipsdist = True + +[testenv] +setenv = + PYTHONWARNINGS = all + +[testenv:linters] +deps = + paramiko + flake8 + flake8-colors + flake8-import-order + flake8-typing-imports + pep8-naming + pyflakes +commands = + flake8 client.py + +[flake8] +ignore = D203, W503, E203 +exclude = + .tox, + .git, + __pycache__, + build, + dist, + *.pyc, + *.egg-info, + .cache, + .eggs, + *migrations* +max-complexity = 10 +max-line-length = 160 +import-order-style = pep8 +application-import-names = flake8 +format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s -- GitLab From 1b045dd14e590093800469e2e96e6a71f6b92955 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 20:34:20 +0200 Subject: [PATCH 14/19] Remove unicode 'u' --- client.py | 160 ++++++++++++++++++++++++++---------------------------- 1 file changed, 78 insertions(+), 82 deletions(-) diff --git a/client.py b/client.py index 354d530..4680fe6 100755 --- a/client.py +++ b/client.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python2 -# -*- encoding: utf-8 -*- +#!/usr/bin/env python3 """Gestion centralisée des mots de passe avec chiffrement GPG @@ -8,8 +7,6 @@ Authors : Daniel Stan Vincent Le Gallic """ -from __future__ import print_function - # Import builtins import sys import subprocess @@ -25,7 +22,6 @@ import time import datetime import copy import logging -from binascii import hexlify from configparser import ConfigParser # Import a SSH client @@ -70,16 +66,16 @@ GPG_ARGS = { #: Mapping (lettre de trustlevel) -> (signification, faut-il faire confiance à la clé) GPG_TRUSTLEVELS = { - u"-" : (u"inconnue (pas de valeur assignée)", False), - u"o" : (u"inconnue (nouvelle clé)", False), - u"i" : (u"invalide (self-signature manquante ?)", False), - u"n" : (u"nulle (il ne faut pas faire confiance à cette clé)", False), - u"m" : (u"marginale (pas assez de lien de confiance vers cette clé)", False), - u"f" : (u"entière (clé dans le réseau de confiance)", True), - u"u" : (u"ultime (c'est probablement ta clé)", True), - u"r" : (u"révoquée", False), - u"e" : (u"expirée", False), - u"q" : (u"non définie", False), + "-" : ("inconnue (pas de valeur assignée)", False), + "o" : ("inconnue (nouvelle clé)", False), + "i" : ("invalide (self-signature manquante ?)", False), + "n" : ("nulle (il ne faut pas faire confiance à cette clé)", False), + "m" : ("marginale (pas assez de lien de confiance vers cette clé)", False), + "f" : ("entière (clé dans le réseau de confiance)", True), + "u" : ("ultime (c'est probablement ta clé)", True), + "r" : ("révoquée", False), + "e" : ("expirée", False), + "q" : ("non définie", False), } def gpg(options, command, args=None): @@ -167,7 +163,7 @@ GPG_PARSERS = { def parse_keys(gpgout, debug=False): """Parse l'output d'un listing de clés gpg.""" ring = {} - init_value = u"initialize" # Valeur utilisée pour dire "cet objet n'a pas encore été rencontré pendant le parsing" + init_value = "initialize" # Valeur utilisée pour dire "cet objet n'a pas encore été rencontré pendant le parsing" current_pub = init_value current_sub = init_value for line in iter(gpgout.readline, ''): @@ -191,35 +187,35 @@ def parse_keys(gpgout, debug=False): except: log.error("*** FAILED *** Line: %s", line) raise - if field == u"pub": + if field == "pub": # Nouvelle clé # On sauvegarde d'abord le dernier sub (si il y en a un) dans son pub parent if current_sub != init_value: current_pub["subkeys"].append(current_sub) # Ensuite on sauve le pub précédent (si il y en a un) dans le ring if current_pub != init_value: - ring[current_pub[u"fpr"]] = current_pub + ring[current_pub["fpr"]] = current_pub # On place le nouveau comme pub courant current_pub = content # Par défaut, il n'a ni subkeys, ni uids - current_pub[u"subkeys"] = [] - current_pub[u"uids"] = [] + current_pub["subkeys"] = [] + current_pub["uids"] = [] # On oublié l'éventuel dernier sub rencontré current_sub = init_value - elif field == u"fpr": + elif field == "fpr": if current_sub != init_value: # On a lu un sub depuis le dernier pub, donc le fingerprint est celui du dernier sub rencontré - current_sub[u"fpr"] = content[u"fpr"] + current_sub["fpr"] = content["fpr"] else: # Alors c'est le fingerprint du pub - current_pub[u"fpr"] = content[u"fpr"] - elif field == u"uid": - current_pub[u"uids"].append(content) - elif field == u"sub": + current_pub["fpr"] = content["fpr"] + elif field == "uid": + current_pub["uids"].append(content) + elif field == "sub": # Nouvelle sous-clé # D'abord on sauvegarde la précédente (si il y en a une) dans son pub parent if current_sub != init_value: - current_pub[u"subkeys"].append(current_sub) + current_pub["subkeys"].append(current_sub) # On place la nouvelle comme sub courant current_sub = content log.debug("current_pub : %r" % current_pub) @@ -230,7 +226,7 @@ def parse_keys(gpgout, debug=False): if current_sub != init_value: current_pub["subkeys"].append(current_sub) if current_pub != init_value: - ring[current_pub[u"fpr"]] = current_pub + ring[current_pub["fpr"]] = current_pub return ring class simple_memoize(object): @@ -361,7 +357,7 @@ 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) distant_username = allroles.pop("whoami") - my_roles = [r for (r, users) in allroles.iteritems() if distant_username in users] + my_roles = [r for (r, users) in allroles.items() if distant_username in users] my_roles_w = [r[:-2] for r in my_roles if r.endswith("-w")] return (my_roles, my_roles_w) @@ -389,21 +385,21 @@ def _check_encryptable(key): Puis qu'on peut chiffrer avec, ou qu'au moins une de ses subkeys est de chiffrement (capability e) et est de confiance et n'est pas expirée""" # Il faut que la clé soit dans le réseau de confiance… - meaning, trustvalue = GPG_TRUSTLEVELS[key[u"trustletter"]] + meaning, trustvalue = GPG_TRUSTLEVELS[key["trustletter"]] if not trustvalue: - return u"La confiance en la clé est : %s" % (meaning,) + return "La confiance en la clé est : %s" % (meaning,) # …et qu'on puisse chiffrer avec… - if u"e" in key[u"capabilities"]: + if "e" in key["capabilities"]: # …soit directement… - return u"" + return "" # …soit avec une de ses subkeys - esubkeys = [sub for sub in key[u"subkeys"] if u"e" in sub[u"capabilities"]] + esubkeys = [sub for sub in key["subkeys"] if "e" in sub["capabilities"]] if len(esubkeys) == 0: - return u"La clé principale de permet pas de chiffrer et auncune sous-clé de chiffrement." - if any([GPG_TRUSTLEVELS[sub[u"trustletter"]][1] for sub in esubkeys]): - return u"" + return "La clé principale de permet pas de chiffrer et auncune sous-clé de chiffrement." + if any([GPG_TRUSTLEVELS[sub["trustletter"]][1] for sub in esubkeys]): + return "" else: - return u"Aucune sous clé de chiffrement n'est de confiance et non expirée." + return "Aucune sous clé de chiffrement n'est de confiance et non expirée." 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, @@ -427,15 +423,15 @@ def check_keys(options, recipients=None, quiet=False): _, gpgout = gpg(options, 'list-keys') localring = parse_keys(gpgout) for (recipient, (mail, fpr)) in keys.iteritems(): - failed = u"" + failed = "" if not fpr is None: if speak: - print((u"Checking %s… " % (mail)).encode("utf-8"), end="") + print(("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 any(["<%s>" % (mail,) in u["uid"] for u in key["uids"]]): if speak: print("M ", end="") # … et qu'on peut raisonnablement chiffrer pour lui @@ -443,16 +439,16 @@ def check_keys(options, recipients=None, quiet=False): if not failed and speak: print("C ", end="") else: - failed = u"!! Le fingerprint et le mail ne correspondent pas !" + failed = "!! Le fingerprint et le mail ne correspondent pas !" else: - failed = u"Pas (ou trop) de clé avec ce fingerprint." + failed = "Pas (ou trop) de clé avec ce fingerprint." if speak: print("") if failed: log.warn("--> Fail on %s:%s\n--> %s" % (mail, fpr, failed)) if not recipients is None: # On cherche à savoir si on droppe ce recipient - message = u"Abandonner le chiffrement pour cette clé ? (Si vous la conservez, il est posible que gpg crashe)" + message = "Abandonner le chiffrement pour cette clé ? (Si vous la conservez, il est posible que gpg crashe)" if confirm(options, message, ('drop', fpr, mail)): drop = True # si on a répondu oui à "abandonner ?", on droppe elif options.drop_invalid and options.force: @@ -475,7 +471,7 @@ def get_recipients_of_roles(options, roles): recipients = set() allroles = all_roles(options) for role in roles: - if role == u"whoami": + if role == "whoami": continue for recipient in allroles[role]: recipients.add(recipient) @@ -484,7 +480,7 @@ def get_recipients_of_roles(options, roles): def get_dest_of_roles(options, roles): """Renvoie la liste des "username : mail (fingerprint)" """ allkeys = all_keys(options) - return [u"%s : %s (%s)" % (rec, allkeys[rec][0], allkeys[rec][1]) + return ["%s : %s (%s)" % (rec, allkeys[rec][0], allkeys[rec][1]) for rec in get_recipients_of_roles(options, roles) if allkeys[rec][1]] def encrypt(options, roles, contents): @@ -505,7 +501,7 @@ def encrypt(options, roles, contents): stdin.close() out = stdout.read().decode("utf-8") if out == '': - return [False, u"Échec de chiffrement"] + return [False, "Échec de chiffrement"] else: return [True, out] @@ -540,7 +536,7 @@ def need_filename(f): NEED_FILENAME.append(f) return f -def editor(texte, annotations=u""): +def editor(texte, annotations=""): """ Lance $EDITOR sur texte. Renvoie le nouveau texte si des modifications ont été apportées, ou None """ @@ -570,40 +566,40 @@ def show_files(options): """Affiche la liste des fichiers disponibles sur le serveur distant""" my_roles, _ = get_my_roles(options) files = all_files(options) - keys = files.keys() + keys = list(files.keys()) keys.sort() - print(u"Liste des fichiers disponibles :".encode("utf-8")) + print("Liste des fichiers disponibles :".encode("utf-8")) for fname in keys: froles = files[fname] access = set(my_roles).intersection(froles) != set([]) - 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")) + print((" %s %s (%s)" % ((access and '+' or '-'), fname, ", ".join(froles))).encode("utf-8")) + print(("""--Mes roles: %s""" % (", ".join(my_roles),)).encode("utf-8")) def restore_files(options): """Restore les fichiers corrompues sur le serveur distant""" - print(u"Fichier corrompus :".encode("utf-8")) + print("Fichier corrompus :".encode("utf-8")) files = restore_all_files(options) keys = files.keys() keys.sort() for fname in keys: - print((u" %s (%s)" % ( fname, files[fname])).encode("utf-8")) + print((" %s (%s)" % ( fname, files[fname])).encode("utf-8")) def show_roles(options): """Affiche la liste des roles existants""" - print(u"Liste des roles disponibles".encode("utf-8")) + print("Liste des roles disponibles".encode("utf-8")) allroles = all_roles(options) for (role, usernames) in allroles.iteritems(): - if role == u"whoami": + if role == "whoami": continue if not role.endswith('-w'): - print((u" * %s : %s" % (role, ", ".join(usernames))).encode("utf-8")) + print((" * %s : %s" % (role, ", ".join(usernames))).encode("utf-8")) def show_servers(options): """Affiche la liste des serveurs disponibles""" - print(u"Liste des serveurs disponibles".encode("utf-8")) + print("Liste des serveurs disponibles".encode("utf-8")) for server in config.keys(): - print((u" * " + server).encode("utf-8")) + print((" * " + server).encode("utf-8")) def saveclipboard(restore=False, old_clipboard=None): """Enregistre le contenu du presse-papier. Le rétablit si ``restore=True``""" @@ -615,7 +611,7 @@ def saveclipboard(restore=False, old_clipboard=None): if not restore: old_clipboard = proc.stdout.read() else: - raw_input(u"Appuyez sur Entrée pour récupérer le contenu précédent du presse papier.".encode("utf-8")) + raw_input("Appuyez sur Entrée pour récupérer le contenu précédent du presse papier.".encode("utf-8")) proc.stdin.write(old_clipboard) proc.stdin.close() proc.stdout.close() @@ -657,7 +653,7 @@ def show_file(options): # Est-ce que le mot de passe a été caché ? (si non, on utilisera less) is_hidden = is_key # Texte avec mdp caché - filtered = u"" + filtered = "" # Ancien contenu du press papier old_clipboard = None @@ -673,12 +669,12 @@ def show_file(options): # On met le mdp dans le clipboard en mémorisant son 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]" + line = "[Le mot de passe a été mis dans le presse papier]" filtered += line + '\n' if is_key: - filtered = u"La clé a été mise dans l'agent ssh" - shown = u"Fichier %s:\n\n%s-----\nVisible par: %s\n" % (fname, filtered, ','.join(passfile['roles'])) + filtered = "La clé a été mise dans l'agent ssh" + shown = "Fichier %s:\n\n%s-----\nVisible par: %s\n" % (fname, filtered, ','.join(passfile['roles'])) if is_key: with tempfile.NamedTemporaryFile(suffix='') as key_file: @@ -717,22 +713,22 @@ def edit_file(options): fname = options.fname gotit, value = get_file(options, fname) nfile = False - annotations = u"" + annotations = "" my_roles, _ = get_my_roles(options) new_roles = options.roles # Cas du nouveau fichier - if not gotit and not u"pas les droits" in value: + if not gotit and not "pas les droits" in value: nfile = True if not options.quiet: - print(u"Fichier introuvable".encode("utf-8")) - if not confirm(options, u"Créer fichier ?"): + print("Fichier introuvable".encode("utf-8")) + if not confirm(options, "Créer fichier ?"): return - annotations += u"""Ceci est un fichier initial contenant un mot de passe + annotations += """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() + texte = "pass: %s\n" % gen_password() if new_roles is None: new_roles = parse_roles(options, cast=True) @@ -763,13 +759,13 @@ Enregistrez le fichier vide pour annuler.\n""" # 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 if not any(r + '-w' in my_roles for r in new_roles): - message = u"""Vous vous apprêtez à perdre vos droits en écriture""" + \ + message = """Vous vous apprêtez à perdre vos droits en écriture""" + \ """(ROLES ne contient rien parmi : %s) sur ce fichier, continuer ?""" message = message % (", ".join(r[:-2] for r in my_roles if '-w' in r),) if not confirm(options, message): return - annotations += u"""Ce fichier sera chiffré pour les rôles suivants :\n%s\n + annotations += """Ce fichier sera chiffré pour les rôles suivants :\n%s\n C'est-à-dire pour les utilisateurs suivants :\n%s""" % ( ', '.join(new_roles), '\n'.join(' %s' % rec for rec in get_dest_of_roles(options, new_roles)) @@ -780,9 +776,9 @@ C'est-à-dire pour les utilisateurs suivants :\n%s""" % ( if ((not nfile and ntexte in [u'', texte] # pas nouveau, vidé ou pas modifié and set(new_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." + message = "Pas de modification à enregistrer.\n" + message += "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 += "Si c'est un nouveau fichier, vous avez tenté de le créer vide." if not options.quiet: print(message.encode("utf-8")) else: @@ -826,8 +822,8 @@ def remove_file(options): 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(options) and u"Base de clés ok" or u"Erreurs dans la base").encode("utf-8")) + print("Vérification que les clés sont valides (uid correspondant au login) et de confiance.") + print((check_keys(options) and "Base de clés ok" or "Erreurs dans la base").encode("utf-8")) def my_update_keys(options): """Met à jour les clés existantes et affiche le résultat""" @@ -870,8 +866,8 @@ def recrypt_files(options, strict=False): # 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"): + message = "Vous vous apprêtez à rechiffrer les fichiers suivants :\n%s" % filenames + if not confirm(options, message + "\nConfirmer"): exit(2) # On rechiffre to_put = [{'filename' : f['filename'], @@ -880,15 +876,15 @@ def recrypt_files(options, strict=False): for [success, f] in files] if to_put: if not options.quiet: - print((u"Rechiffrement de %s" % (", ".join([f['filename'] for f in to_put]))).encode("utf-8")) + print(("Rechiffrement de %s" % (", ".join([f['filename'] for f in to_put]))).encode("utf-8")) results = put_files(options, to_put) # On affiche les messages de retour if not options.quiet: for i in range(len(results)): - print(u"%s : %s" % (to_put[i]['filename'], results[i][1])) + print("%s : %s" % (to_put[i]['filename'], results[i][1])) else: if not options.quiet: - print(u"Aucun fichier n'a besoin d'être rechiffré".encode("utf-8")) + print("Aucun fichier n'a besoin d'être rechiffré".encode("utf-8")) def parse_roles(options, cast=False): """Interprête la liste de rôles fournie par l'utilisateur. -- GitLab From b031c6801f62b6b0bedd0a1767f5df74b0cea9ed Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 20:50:46 +0200 Subject: [PATCH 15/19] Remove encode/decode from print/input --- client.py | 52 ++++++++++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/client.py b/client.py index 4680fe6..b4a6d07 100755 --- a/client.py +++ b/client.py @@ -2,7 +2,7 @@ """Gestion centralisée des mots de passe avec chiffrement GPG -Copyright (C) 2010-2013 Cr@ns +Copyright (C) 2010-2020 Cr@ns Authors : Daniel Stan Vincent Le Gallic """ @@ -301,7 +301,7 @@ def remote_command(options, command, arg=None, stdin_contents=None): ret = stdout.channel.recv_exit_status() if ret != 0: err = "" - if sterr.channel.recv_stderr_ready(): + if stderr.channel.recv_stderr_ready(): err = stderr.read() log.error("Wrong server return code %s, error is %s" % (ret, err)) exit(ret) @@ -366,7 +366,7 @@ def gen_password(): random.seed(datetime.datetime.now().microsecond) chars = string.letters + string.digits + '/=+*' length = 15 - return u''.join([random.choice(chars) for _ in xrange(length)]) + return u''.join([random.choice(chars) for _ in range(length)]) ###### ## Local commands @@ -426,7 +426,7 @@ def check_keys(options, recipients=None, quiet=False): failed = "" if not fpr is None: if speak: - print(("Checking %s… " % (mail)).encode("utf-8"), end="") + print("Checking %s… " % mail, end="") key = localring.get(fpr, None) # On vérifie qu'on possède la clé… if not key is None: @@ -508,9 +508,6 @@ def encrypt(options, roles, contents): def decrypt(options, contents): """Déchiffre le contenu""" stdin, stdout = gpg(options, "decrypt") - if type(contents) != unicode: # Kludge (broken db ?) - log.warn("Eau dans le gaz (decrypt)" + repr(contents)) - contents = contents[-1] stdin.write(contents.encode("utf-8")) stdin.close() return stdout.read().decode("utf-8") @@ -568,38 +565,38 @@ def show_files(options): files = all_files(options) keys = list(files.keys()) keys.sort() - print("Liste des fichiers disponibles :".encode("utf-8")) + print("Liste des fichiers disponibles :") for fname in keys: froles = files[fname] access = set(my_roles).intersection(froles) != set([]) - print((" %s %s (%s)" % ((access and '+' or '-'), fname, ", ".join(froles))).encode("utf-8")) - print(("""--Mes roles: %s""" % (", ".join(my_roles),)).encode("utf-8")) + print((" %s %s (%s)" % ((access and '+' or '-'), fname, ", ".join(froles)))) + print(("""--Mes roles: %s""" % (", ".join(my_roles),))) def restore_files(options): """Restore les fichiers corrompues sur le serveur distant""" - print("Fichier corrompus :".encode("utf-8")) + print("Fichier corrompus :") files = restore_all_files(options) keys = files.keys() keys.sort() for fname in keys: - print((" %s (%s)" % ( fname, files[fname])).encode("utf-8")) + print(" %s (%s)" % ( fname, files[fname])) def show_roles(options): """Affiche la liste des roles existants""" - print("Liste des roles disponibles".encode("utf-8")) + print("Liste des roles disponibles") allroles = all_roles(options) for (role, usernames) in allroles.iteritems(): if role == "whoami": continue if not role.endswith('-w'): - print((" * %s : %s" % (role, ", ".join(usernames))).encode("utf-8")) + print(" * %s : %s" % (role, ", ".join(usernames))) def show_servers(options): """Affiche la liste des serveurs disponibles""" - print("Liste des serveurs disponibles".encode("utf-8")) + print("Liste des serveurs disponibles") for server in config.keys(): - print((" * " + server).encode("utf-8")) + print(" * " + server) def saveclipboard(restore=False, old_clipboard=None): """Enregistre le contenu du presse-papier. Le rétablit si ``restore=True``""" @@ -611,7 +608,7 @@ def saveclipboard(restore=False, old_clipboard=None): if not restore: old_clipboard = proc.stdout.read() else: - raw_input("Appuyez sur Entrée pour récupérer le contenu précédent du presse papier.".encode("utf-8")) + input("Appuyez sur Entrée pour récupérer le contenu précédent du presse papier.") proc.stdin.write(old_clipboard) proc.stdin.close() proc.stdout.close() @@ -687,7 +684,7 @@ def show_file(options): # On attend (hors tmpfile) print(shown.encode('utf-8')) - raw_input() + input() with tempfile.NamedTemporaryFile(suffix='') as pub_file: # On met la clé publique en fichier pour suppression pub_file.write(pub) @@ -722,7 +719,7 @@ def edit_file(options): if not gotit and not "pas les droits" in value: nfile = True if not options.quiet: - print("Fichier introuvable".encode("utf-8")) + print("Fichier introuvable") if not confirm(options, "Créer fichier ?"): return annotations += """Ceci est un fichier initial contenant un mot de passe @@ -780,11 +777,11 @@ C'est-à-dire pour les utilisateurs suivants :\n%s""" % ( message += "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 += "Si c'est un nouveau fichier, vous avez tenté de le créer vide." if not options.quiet: - print(message.encode("utf-8")) + print(message) else: ntexte = texte if ntexte == None else ntexte success, message = put_password(options, new_roles, ntexte) - print(message.encode("utf-8")) + print(message) _remember_dict = {} def confirm(options, text, remember_key=None): @@ -798,7 +795,7 @@ def confirm(options, text, remember_key=None): if remember_key in _remember_dict: return _remember_dict[remember_key] while True: - out = raw_input((text + u' (o/n)').encode("utf-8")).lower() + out = input((text + u' (o/n)')).lower() if out == 'o': res = True break @@ -817,17 +814,17 @@ def remove_file(options): if not confirm(options, u'Êtes-vous sûr de vouloir supprimer %s ?' % (fname,)): return message = rm_file(fname) - print(message.encode("utf-8")) + print(message) def my_check_keys(options): """Vérifie les clés et affiche un message en fonction du résultat""" print("Vérification que les clés sont valides (uid correspondant au login) et de confiance.") - print((check_keys(options) and "Base de clés ok" or "Erreurs dans la base").encode("utf-8")) + print(check_keys(options) and "Base de clés ok" or "Erreurs dans la base") def my_update_keys(options): """Met à jour les clés existantes et affiche le résultat""" - print(update_keys(options).encode("utf-8")) + print(update_keys(options)) def recrypt_files(options, strict=False): """Rechiffre les fichiers. @@ -876,7 +873,7 @@ def recrypt_files(options, strict=False): for [success, f] in files] if to_put: if not options.quiet: - print(("Rechiffrement de %s" % (", ".join([f['filename'] for f in to_put]))).encode("utf-8")) + print("Rechiffrement de %s" % (", ".join([f['filename'] for f in to_put]))) results = put_files(options, to_put) # On affiche les messages de retour if not options.quiet: @@ -884,7 +881,7 @@ def recrypt_files(options, strict=False): print("%s : %s" % (to_put[i]['filename'], results[i][1])) else: if not options.quiet: - print("Aucun fichier n'a besoin d'être rechiffré".encode("utf-8")) + print("Aucun fichier n'a besoin d'être rechiffré") def parse_roles(options, cast=False): """Interprête la liste de rôles fournie par l'utilisateur. @@ -1006,7 +1003,6 @@ if __name__ == "__main__": liste """) parser.add_argument('fname', nargs='?', default=None, - type=lambda x: x.decode('utf-8'), help="Nom du fichier à afficher") # On parse les options fournies en commandline -- GitLab From 2092fba195aaca6227366bfd9366feeca83ffee8 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 20:51:05 +0200 Subject: [PATCH 16/19] README: use py3 deps --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index b81b26b..cb5d4b4 100644 --- a/README +++ b/README @@ -10,7 +10,7 @@ avant de lancer make install ou make install-server. == Installation et configuration du client == -Sous Debian il faut avoir `python2.7` et `python-paramiko` d'installés. +Sous Debian il faut avoir `python3` et `python3-paramiko` d'installés. * Copiez le dépôt git sur votre machine : $ git clone git@gitlab.crans.org:nounous/cranspasswords.git -- GitLab From d87d4b86f7378f6e401a41d496ec91b33635a2bf Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 21:03:18 +0200 Subject: [PATCH 17/19] Format with autopep8 --- client.py | 422 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 280 insertions(+), 142 deletions(-) diff --git a/client.py b/client.py index b4a6d07..400f7db 100755 --- a/client.py +++ b/client.py @@ -32,16 +32,18 @@ from paramiko.ssh_exception import SSHException # On n'a pas encore accès à la config donc on devine le nom bootstrap_cmd_name = os.path.split(sys.argv[0])[1] default_config_path = os.path.expanduser("~/.config/" + bootstrap_cmd_name) -config_path = os.getenv("CRANSPASSWORDS_CLIENT_CONFIG_DIR", default_config_path) +config_path = os.getenv( + "CRANSPASSWORDS_CLIENT_CONFIG_DIR", default_config_path) config = ConfigParser() if not config.read(config_path + "/clientconfig.ini"): # If config could not be imported, display an error if required ducktape_display_error = sys.stderr.isatty() and \ - not any([opt in sys.argv for opt in ["-q", "--quiet"]]) and \ - __name__ == '__main__' + not any([opt in sys.argv for opt in ["-q", "--quiet"]]) and \ + __name__ == '__main__' if ducktape_display_error: # Do not use logger as it has not been initialized yet - print("%s/clientconfig.ini could not be read. Please read README." % config_path) + print("%s/clientconfig.ini could not be read. Please read README." % + config_path) exit(1) # Logger local @@ -49,34 +51,35 @@ log = logging.getLogger(bootstrap_cmd_name) #: 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) + flags=re.IGNORECASE) -## GPG Definitions +# GPG Definitions #: Path du binaire gpg GPG = 'gpg' #: Paramètres à fournir à gpg en fonction de l'action désirée GPG_ARGS = { - 'decrypt' : ['-d'], - 'encrypt' : ['--armor', '-es'], - 'receive-keys' : ['--recv-keys'], - 'list-keys' : ['--list-keys', '--with-colons', '--fixed-list-mode', - '--with-fingerprint', '--with-fingerprint'], # Ce n'est pas une erreur. Il faut 2 --with-fingerprint pour avoir les fingerprints des subkeys. - } + 'decrypt': ['-d'], + 'encrypt': ['--armor', '-es'], + 'receive-keys': ['--recv-keys'], + 'list-keys': ['--list-keys', '--with-colons', '--fixed-list-mode', + '--with-fingerprint', '--with-fingerprint'], # Ce n'est pas une erreur. Il faut 2 --with-fingerprint pour avoir les fingerprints des subkeys. +} #: Mapping (lettre de trustlevel) -> (signification, faut-il faire confiance à la clé) GPG_TRUSTLEVELS = { - "-" : ("inconnue (pas de valeur assignée)", False), - "o" : ("inconnue (nouvelle clé)", False), - "i" : ("invalide (self-signature manquante ?)", False), - "n" : ("nulle (il ne faut pas faire confiance à cette clé)", False), - "m" : ("marginale (pas assez de lien de confiance vers cette clé)", False), - "f" : ("entière (clé dans le réseau de confiance)", True), - "u" : ("ultime (c'est probablement ta clé)", True), - "r" : ("révoquée", False), - "e" : ("expirée", False), - "q" : ("non définie", False), - } + "-": ("inconnue (pas de valeur assignée)", False), + "o": ("inconnue (nouvelle clé)", False), + "i": ("invalide (self-signature manquante ?)", False), + "n": ("nulle (il ne faut pas faire confiance à cette clé)", False), + "m": ("marginale (pas assez de lien de confiance vers cette clé)", False), + "f": ("entière (clé dans le réseau de confiance)", True), + "u": ("ultime (c'est probablement ta clé)", True), + "r": ("révoquée", False), + "e": ("expirée", False), + "q": ("non définie", False), +} + def gpg(options, command, args=None): """Lance gpg pour la commande donnée avec les arguments @@ -94,76 +97,84 @@ def gpg(options, command, args=None): # Run gpg log.info("Running `%s`" % " ".join(full_command)) proc = subprocess.Popen(full_command, - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = stderr, - close_fds = True) + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=stderr, + close_fds=True) if not options.verbose: proc.stderr.close() return proc.stdin, proc.stdout + def _parse_timestamp(string, canbenone=False): """Interprète ``string`` comme un timestamp depuis l'Epoch.""" if string == u'' and canbenone: return None return datetime.datetime(*time.localtime(int(string))[:7]) + def _parse_pub(data): """Interprète une ligne ``pub:``""" d = { - u'trustletter' : data[1], - u'length' : int(data[2]), - u'algorithm' : int(data[3]), - u'longid' : data[4], - u'signdate' : _parse_timestamp(data[5]), - u'expiredate' : _parse_timestamp(data[6], canbenone=True), - u'ownertrustletter' : data[8], - u'capabilities' : data[11], - } + u'trustletter': data[1], + u'length': int(data[2]), + u'algorithm': int(data[3]), + u'longid': data[4], + u'signdate': _parse_timestamp(data[5]), + u'expiredate': _parse_timestamp(data[6], canbenone=True), + u'ownertrustletter': data[8], + u'capabilities': data[11], + } return d + def _parse_uid(data): """Interprète une ligne ``uid:``""" d = { - u'trustletter' : data[1], - u'signdate' : _parse_timestamp(data[5], canbenone=True), - u'hash' : data[7], - u'uid' : data[9], - } + u'trustletter': data[1], + u'signdate': _parse_timestamp(data[5], canbenone=True), + u'hash': data[7], + u'uid': data[9], + } return d + def _parse_fpr(data): """Interprète une ligne ``fpr:``""" d = { - u'fpr' : data[9], - } + u'fpr': data[9], + } return d + def _parse_sub(data): """Interprète une ligne ``sub:``""" d = { - u'trustletter' : data[1], - u'length' : int(data[2]), - u'algorithm' : int(data[3]), - u'longid' : data[4], - u'signdate' : _parse_timestamp(data[5]), - u'expiredate' : _parse_timestamp(data[6], canbenone=True), - u'capabilities' : data[11], - } + u'trustletter': data[1], + u'length': int(data[2]), + u'algorithm': int(data[3]), + u'longid': data[4], + u'signdate': _parse_timestamp(data[5]), + u'expiredate': _parse_timestamp(data[6], canbenone=True), + u'capabilities': data[11], + } return d + #: Functions to parse the recognized fields GPG_PARSERS = { - u'pub' : _parse_pub, - u'uid' : _parse_uid, - u'fpr' : _parse_fpr, - u'sub' : _parse_sub, - } + u'pub': _parse_pub, + u'uid': _parse_uid, + u'fpr': _parse_fpr, + u'sub': _parse_sub, +} + def parse_keys(gpgout, debug=False): """Parse l'output d'un listing de clés gpg.""" ring = {} - init_value = "initialize" # Valeur utilisée pour dire "cet objet n'a pas encore été rencontré pendant le parsing" + # Valeur utilisée pour dire "cet objet n'a pas encore été rencontré pendant le parsing" + init_value = "initialize" current_pub = init_value current_sub = init_value for line in iter(gpgout.readline, ''): @@ -175,7 +186,8 @@ def parse_keys(gpgout, debug=False): line = line.decode("iso8859-1") except UnicodeDecodeError: line = line.decode("iso8859-1", "ignore") - log.warning("gpg is telling shit, it is neither ISO-8859-1 nor UTF-8. Dropping!") + log.warning( + "gpg is telling shit, it is neither ISO-8859-1 nor UTF-8. Dropping!") line = line.split(":") field = line[0] if field in GPG_PARSERS.keys(): @@ -229,8 +241,10 @@ def parse_keys(gpgout, debug=False): ring[current_pub["fpr"]] = current_pub return ring + class simple_memoize(object): """ Memoization/Lazy """ + def __init__(self, f): self.f = f self.val = None @@ -251,7 +265,7 @@ class simple_memoize(object): ###### -## Remote commands +# Remote commands @simple_memoize def create_ssh_client(): @@ -268,11 +282,13 @@ def create_ssh_client(): try: client.connect(str(options.serverdata['host'])) except SSHException: - log.error("Host key is unknown or you are using a outdated python-paramiko (ssh-ed25519 was implemented in 2017)") + log.error( + "Host key is unknown or you are using a outdated python-paramiko (ssh-ed25519 was implemented in 2017)") raise return client + def remote_command(options, command, arg=None, stdin_contents=None): """ Execute remote command and return output @@ -322,21 +338,25 @@ def all_keys(options): """Récupère les clés du serveur distant""" return remote_command(options, "listkeys") + @simple_memoize def all_roles(options): """Récupère les roles du serveur distant""" return remote_command(options, "listroles") + @simple_memoize def all_files(options): """Récupère les fichiers du serveur distant""" return remote_command(options, "listfiles") + @simple_memoize def restore_all_files(options): """Récupère les fichiers du serveur distant""" return remote_command(options, "restorefiles") + @simple_memoize def get_file(options, filename): """ @@ -344,23 +364,28 @@ def get_file(options, filename): """ return remote_command(options, "getfile", filename) + def put_files(options, files): """Dépose les fichiers sur le serveur distant""" return remote_command(options, "putfiles", stdin_contents=files) + def rm_file(filename): """Supprime le fichier sur le serveur distant""" return remote_command(options, "rmfile", filename) + @simple_memoize 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) distant_username = allroles.pop("whoami") - my_roles = [r for (r, users) in allroles.items() if distant_username in users] + my_roles = [r for (r, users) in allroles.items() + if distant_username 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""" random.seed(datetime.datetime.now().microsecond) @@ -369,16 +394,19 @@ def gen_password(): return u''.join([random.choice(chars) for _ in range(length)]) ###### -## Local commands +# Local commands + def update_keys(options): """Met à jour les clés existantes""" keys = all_keys(options) - _, stdout = gpg(options, "receive-keys", [key for _, key in keys.values() if key]) + _, stdout = gpg(options, "receive-keys", + [key for _, key in keys.values() if key]) return stdout.read().decode("utf-8") + def _check_encryptable(key): """Vérifie qu'on peut chiffrer un message pour ``key``. C'est-à-dire, que la clé est de confiance (et non expirée). @@ -401,6 +429,7 @@ def _check_encryptable(key): else: return "Aucune sous clé de chiffrement n'est de confiance et non expirée." + 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). @@ -417,7 +446,7 @@ def check_keys(options, recipients=None, quiet=False): speak = options.verbose and not options.quiet else: speak = False - keys = {u : val for (u, val) in keys.iteritems() if u in recipients} + keys = {u: val for (u, val) in keys.iteritems() if u in recipients} if speak: print("M : le mail correspond à un uid du fingerprint\nC : confiance OK (inclut la vérification de non expiration).\n") _, gpgout = gpg(options, 'list-keys') @@ -450,11 +479,11 @@ def check_keys(options, recipients=None, quiet=False): # On cherche à savoir si on droppe ce recipient message = "Abandonner le chiffrement pour cette clé ? (Si vous la conservez, il est posible que gpg crashe)" if confirm(options, message, ('drop', fpr, mail)): - drop = True # si on a répondu oui à "abandonner ?", on droppe + drop = True # si on a répondu oui à "abandonner ?", on droppe elif options.drop_invalid and options.force: - drop = True # ou bien si --drop-invalid avec --force nous autorisent à dropper silencieusement + drop = True # ou bien si --drop-invalid avec --force nous autorisent à dropper silencieusement else: - drop = False # Là, on droppe pas + drop = False # Là, on droppe pas if not drop: trusted_recipients.append(recipient) else: @@ -466,6 +495,7 @@ def check_keys(options, recipients=None, quiet=False): else: return trusted_recipients + def get_recipients_of_roles(options, roles): """Renvoie les destinataires d'une liste de rôles""" recipients = set() @@ -477,11 +507,13 @@ def get_recipients_of_roles(options, roles): recipients.add(recipient) return recipients + def get_dest_of_roles(options, roles): """Renvoie la liste des "username : mail (fingerprint)" """ allkeys = all_keys(options) return ["%s : %s (%s)" % (rec, allkeys[rec][0], allkeys[rec][1]) - for rec in get_recipients_of_roles(options, roles) if allkeys[rec][1]] + for rec in get_recipients_of_roles(options, roles) if allkeys[rec][1]] + def encrypt(options, roles, contents): """Chiffre le contenu pour les roles donnés""" @@ -505,6 +537,7 @@ def encrypt(options, roles, contents): else: return [True, out] + def decrypt(options, contents): """Déchiffre le contenu""" stdin, stdout = gpg(options, "decrypt") @@ -512,27 +545,31 @@ def decrypt(options, contents): stdin.close() return stdout.read().decode("utf-8") + def put_password(options, roles, contents): """Dépose le mot de passe après l'avoir chiffré pour les destinataires dans ``roles``.""" success, enc_pwd_or_error = encrypt(options, roles, contents) if success: enc_pwd = enc_pwd_or_error - return put_files(options, [{'filename' : options.fname, '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] ###### -## Interface +# 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=""): """ Lance $EDITOR sur texte. Renvoie le nouveau texte si des modifications ont été apportées, ou None @@ -556,9 +593,11 @@ def editor(texte, annotations=""): f.seek(0) ntexte = f.read().decode("utf-8", errors='ignore') f.close() - ntexte = u'\n'.join(filter(lambda l: not l.startswith('#'), ntexte.split('\n'))) + ntexte = u'\n'.join( + filter(lambda l: not l.startswith('#'), ntexte.split('\n'))) return ntexte + def show_files(options): """Affiche la liste des fichiers disponibles sur le serveur distant""" my_roles, _ = get_my_roles(options) @@ -572,6 +611,7 @@ def show_files(options): print((" %s %s (%s)" % ((access and '+' or '-'), fname, ", ".join(froles)))) print(("""--Mes roles: %s""" % (", ".join(my_roles),))) + def restore_files(options): """Restore les fichiers corrompues sur le serveur distant""" print("Fichier corrompus :") @@ -579,32 +619,34 @@ def restore_files(options): keys = files.keys() keys.sort() for fname in keys: - print(" %s (%s)" % ( fname, files[fname])) + print(" %s (%s)" % (fname, files[fname])) def show_roles(options): """Affiche la liste des roles existants""" print("Liste des roles disponibles") - allroles = all_roles(options) + allroles = all_roles(options) for (role, usernames) in allroles.iteritems(): if role == "whoami": continue if not role.endswith('-w'): print(" * %s : %s" % (role, ", ".join(usernames))) + def show_servers(options): """Affiche la liste des serveurs disponibles""" print("Liste des serveurs disponibles") for server in config.keys(): print(" * " + server) + def saveclipboard(restore=False, old_clipboard=None): """Enregistre le contenu du presse-papier. Le rétablit si ``restore=True``""" if restore and old_clipboard == None: return act = '-in' if restore else '-out' proc = subprocess.Popen(['xclip', act, '-selection', 'clipboard'], - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr) + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr) if not restore: old_clipboard = proc.stdout.read() else: @@ -614,27 +656,29 @@ def saveclipboard(restore=False, old_clipboard=None): proc.stdout.close() return old_clipboard + def clipboard(texte): """Place ``texte`` dans le presse-papier en mémorisant l'ancien contenu.""" old_clipboard = saveclipboard() - proc =subprocess.Popen(['xclip', '-selection', 'clipboard'],\ - stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr) + 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 old_clipboard + @need_filename def show_file(options): """Affiche le contenu d'un fichier""" fname = options.fname gotit, value = get_file(options, fname) if not gotit: - log.warn(value) # value contient le message d'erreur + log.warn(value) # value contient le message d'erreur return passfile = value (sin, sout) = gpg(options, 'decrypt') content = passfile['contents'] - + # Kludge (broken db ?) if type(content) == list: log.warn("Eau dans le gaz") @@ -671,14 +715,16 @@ def show_file(options): if is_key: filtered = "La clé a été mise dans l'agent ssh" - shown = "Fichier %s:\n\n%s-----\nVisible par: %s\n" % (fname, filtered, ','.join(passfile['roles'])) + shown = "Fichier %s:\n\n%s-----\nVisible par: %s\n" % ( + fname, filtered, ','.join(passfile['roles'])) if is_key: with tempfile.NamedTemporaryFile(suffix='') as key_file: # Génère la clé publique correspondante key_file.write(texte.encode('utf-8')) key_file.flush() - pub = subprocess.check_output(['ssh-keygen', '-y', '-f', key_file.name]) + pub = subprocess.check_output( + ['ssh-keygen', '-y', '-f', key_file.name]) # Charge en mémoire subprocess.check_call(['ssh-add', key_file.name]) @@ -704,6 +750,7 @@ def show_file(options): if old_clipboard is not None: saveclipboard(restore=True, old_clipboard=old_clipboard) + @need_filename def edit_file(options): """Modifie/Crée un fichier""" @@ -729,7 +776,7 @@ Enregistrez le fichier vide pour annuler.\n""" if new_roles is None: new_roles = parse_roles(options, cast=True) - passfile = {'roles' : new_roles} + passfile = {'roles': new_roles} elif not gotit: log.warn(value) return @@ -764,15 +811,16 @@ Enregistrez le fichier vide pour annuler.\n""" annotations += """Ce fichier sera chiffré pour les rôles suivants :\n%s\n C'est-à-dire pour les utilisateurs suivants :\n%s""" % ( - ', '.join(new_roles), - '\n'.join(' %s' % rec for rec in get_dest_of_roles(options, new_roles)) - ) + ', '.join(new_roles), + '\n'.join(' %s' % + rec for rec in get_dest_of_roles(options, new_roles)) + ) ntexte = editor(texte, annotations) if ((not nfile and ntexte in [u'', texte] # pas nouveau, vidé ou pas modifié - and set(new_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. + and set(new_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 = "Pas de modification à enregistrer.\n" message += "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 += "Si c'est un nouveau fichier, vous avez tenté de le créer vide." @@ -783,7 +831,10 @@ C'est-à-dire pour les utilisateurs suivants :\n%s""" % ( success, message = put_password(options, new_roles, ntexte) print(message) + _remember_dict = {} + + def confirm(options, text, remember_key=None): """Demande confirmation, sauf si on est mode ``--force``. Si ``remember_key`` est fourni, il doit correspondre à un objet hashable @@ -807,6 +858,7 @@ def confirm(options, text, remember_key=None): _remember_dict[remember_key] = res return res + @need_filename def remove_file(options): """Supprime un fichier""" @@ -822,15 +874,17 @@ def my_check_keys(options): print("Vérification que les clés sont valides (uid correspondant au login) et de confiance.") print(check_keys(options) and "Base de clés ok" or "Erreurs dans la base") + def my_update_keys(options): """Met à jour les clés existantes et affiche le résultat""" print(update_keys(options)) + def recrypt_files(options, strict=False): """Rechiffre les fichiers. Ici, la signification de ``options.roles`` est : - strict => on chiffre les fichiers dont *tous* les rôles sont dans la liste - non strict => on ne veut rechiffrer que les fichiers qui ont au moins un de ces roles. + strict => on chiffre les fichiers dont *tous* les rôles sont dans la liste + non strict => 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) @@ -851,7 +905,7 @@ def recrypt_files(options, strict=False): return bool(set(fileroles).intersection(rechiffre_roles)) askfiles = [filename for (filename, fileroles) in allfiles.iteritems() - if is_wanted(fileroles) ] + if is_wanted(fileroles)] files = [get_file(options, f) for f in askfiles] # Au cas où on aurait échoué à récupérer ne serait-ce qu'un de ces fichiers, @@ -867,13 +921,14 @@ def recrypt_files(options, strict=False): if not confirm(options, message + "\nConfirmer"): exit(2) # On rechiffre - to_put = [{'filename' : f['filename'], - 'roles' : f['roles'], - 'contents' : encrypt(options, f['roles'], decrypt(options, f['contents']))[-1]} + to_put = [{'filename': f['filename'], + 'roles': f['roles'], + 'contents': encrypt(options, f['roles'], decrypt(options, f['contents']))[-1]} for [success, f] in files] if to_put: if not options.quiet: - print("Rechiffrement de %s" % (", ".join([f['filename'] for f in to_put]))) + print("Rechiffrement de %s" % + (", ".join([f['filename'] for f in to_put]))) results = put_files(options, to_put) # On affiche les messages de retour if not options.quiet: @@ -883,6 +938,7 @@ def recrypt_files(options, strict=False): if not options.quiet: print("Aucun fichier n'a besoin d'être rechiffré") + def parse_roles(options, cast=False): """Interprête la liste de rôles fournie par l'utilisateur. Si il n'en a pas fourni, c'est-à-dire que roles @@ -911,11 +967,13 @@ def parse_roles(options, cast=False): log.warn("role %s do not exists" % role) exit(1) if role.endswith('-w'): - log.warn("role %s should not be used, rather use %s" % (role, role[:-2])) + log.warn("role %s should not be used, rather use %s" % + (role, role[:-2])) exit(1) ret.add(role) 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: @@ -924,6 +982,7 @@ def insult_on_nofilename(options, parser): parser.print_help() exit(1) + if __name__ == "__main__": # Gestion des arguments parser = argparse.ArgumentParser( @@ -941,58 +1000,132 @@ if __name__ == "__main__": default=False, help="silent mode: hide errors, overrides verbosity" ) - parser.add_argument('-s', '--server', default='DEFAULT', - help="Utilisation d'un serveur alternatif (test, backup, etc)") - parser.add_argument('--drop-invalid', action='store_true', default=False, + parser.add_argument( + '-s', '--server', + default='DEFAULT', + help="Utilisation d'un serveur alternatif (test, backup, etc)" + ) + 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('-c', '--clipboard', action='store_true', default=None, + help="Combiné avec --force, droppe les clés en lesquelles on n'a pas confiance sans demander confirmation." + ) + 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, + parser.add_argument( + '--no-clip', '--noclip', '--noclipboard', + action='store_false', + default=None, dest='clipboard', - 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") + 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" + ) # Actions possibles action_grp = parser.add_mutually_exclusive_group(required=False) - action_grp.add_argument('-e', '--edit', action='store_const', dest='action', - default=show_file, const=edit_file, - help="Editer (ou créer)") - action_grp.add_argument('--view', action='store_const', dest='action', - default=show_file, const=show_file, - help="Voir le fichier") - action_grp.add_argument('--remove', action='store_const', dest='action', - default=show_file, const=remove_file, - help="Effacer le fichier") - action_grp.add_argument('-l', '--list', action='store_const', dest='action', - default=show_file, const=show_files, - help="Lister les fichiers") - action_grp.add_argument('-r', '--restore', action='store_const', dest='action', - default=show_file, const=restore_files, - help="Restorer les fichiers corrompues") - action_grp.add_argument('--check-keys', action='store_const', dest='action', - default=show_file, const=my_check_keys, - help="Vérifier les clés") - action_grp.add_argument('--update-keys', action='store_const', dest='action', - default=show_file, const=my_update_keys, - help="Mettre à jour les clés") - action_grp.add_argument('--list-roles', action='store_const', dest='action', - default=show_file, const=show_roles, - help="Lister les rôles existants") - action_grp.add_argument('--list-servers', action='store_const', dest='action', - default=show_file, const=show_servers, + action_grp.add_argument( + '-e', '--edit', + action='store_const', + dest='action', + default=show_file, + const=edit_file, + help="Editer (ou créer)" + ) + action_grp.add_argument( + '--view', + action='store_const', + dest='action', + default=show_file, + const=show_file, + help="Voir le fichier" + ) + action_grp.add_argument( + '--remove', + action='store_const', + dest='action', + default=show_file, + const=remove_file, + help="Effacer le fichier" + ) + action_grp.add_argument( + '-l', '--list', + action='store_const', + dest='action', + default=show_file, + const=show_files, + help="Lister les fichiers" + ) + action_grp.add_argument( + '-r', '--restore', + action='store_const', dest='action', + default=show_file, + const=restore_files, + help="Restorer les fichiers corrompues" + ) + action_grp.add_argument( + '--check-keys', + + action='store_const', + dest='action', + default=show_file, + const=my_check_keys, + help="Vérifier les clés" + ) + action_grp.add_argument( + '--update-keys', + action='store_const', dest='action', + default=show_file, + const=my_update_keys, + help="Mettre à jour les clés" + ) + action_grp.add_argument( + '--list-roles', + action='store_const', + dest='action', + default=show_file, + const=show_roles, + help="Lister les rôles existants" + ) + action_grp.add_argument( + '--list-servers', + action='store_const', + dest='action', + default=show_file, + const=show_servers, help="Lister les serveurs") - action_grp.add_argument('--recrypt-files', action='store_const', dest='action', - default=show_file, const=recrypt_files, + action_grp.add_argument( + '--recrypt-files', + action='store_const', + dest='action', + default=show_file, + const=recrypt_files, 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é)""") - action_grp.add_argument('--strict-recrypt-files', action='store_const', dest='action', - default=show_file, const=lambda x:recrypt_files(x, strict=True), - help="""Rechiffrer les mots de passe (mode strict, voir --roles)""") + Cela sert à mettre à jour les recipients pour qui un password est chiffré)""" + ) + action_grp.add_argument( + '--strict-recrypt-files', + action='store_const', + dest='action', + default=show_file, const=lambda x: recrypt_files( + x, strict=True), + help="""Rechiffrer les mots de passe (mode strict, voir --roles)""" + ) - parser.add_argument('--roles', nargs='?', default=None, + parser.add_argument( + '--roles', + nargs='?', + default=None, help="""Liste de roles (séparés par des virgules). Par défaut, tous les rôles en écriture (sauf pour l'édition, d'un fichier existant). Avec --edit: le fichier sera chiffré pour exactement ces roles @@ -1002,8 +1135,12 @@ if __name__ == "__main__": * strict: tout fichier dont *tous* les rôles sont dans la liste """) - parser.add_argument('fname', nargs='?', default=None, - help="Nom du fichier à afficher") + parser.add_argument( + 'fname', + nargs='?', + default=None, + help="Nom du fichier à afficher" + ) # On parse les options fournies en commandline options = parser.parse_args() @@ -1020,15 +1157,16 @@ if __name__ == "__main__": format='\033[90m%(asctime)s\033[1;0m %(name)s %(levelname)s %(message)s' ) - ## 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 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. # ** Presse papier ** # Si l'utilisateur n'a rien dit (ni option --clipboard ni --noclipboard), # on active le clipboard par défaut, à la condition # que xclip existe et qu'il a un serveur graphique auquel parler. if options.clipboard is None: - options.clipboard = bool(os.getenv('DISPLAY')) and os.path.exists('/usr/bin/xclip') + options.clipboard = 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[options.server] # On parse les roles fournis, et il doivent exister, ne pas être -w… -- GitLab From df2b9aff3ab8c4e1d5f6b02ab813237b2126e06b Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 21:18:57 +0200 Subject: [PATCH 18/19] Fix is None conditions --- client.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/client.py b/client.py index 400f7db..5ba7735 100755 --- a/client.py +++ b/client.py @@ -196,7 +196,7 @@ def parse_keys(gpgout, debug=False): log.debug("current_sub : %r" % current_sub) try: content = GPG_PARSERS[field](line) - except: + except KeyError: log.error("*** FAILED *** Line: %s", line) raise if field == "pub": @@ -254,7 +254,7 @@ class simple_memoize(object): 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: + if self.val is None: self.val = self.f(*args, **kwargs) # On évite de tout deepcopier. Typiquement, un SSHClient # ne devrait pas l'être (comme dans create_ssh_client) @@ -272,7 +272,7 @@ def create_ssh_client(): """ Create a SSH client with paramiko module """ - if not "host" in options.serverdata: + if "host" not in options.serverdata: log.error("Missing parameter `host` in active server configuration") exit(1) @@ -296,7 +296,7 @@ def remote_command(options, command, arg=None, stdin_contents=None): client = create_ssh_client() # Build command - if not "remote_cmd" in options.serverdata: + if "remote_cmd" not in options.serverdata: log.error("Missing parameter `remote_cmd` in active server configuration") exit(1) remote_cmd = options.serverdata['remote_cmd'] + " " + command @@ -453,12 +453,12 @@ def check_keys(options, recipients=None, quiet=False): localring = parse_keys(gpgout) for (recipient, (mail, fpr)) in keys.iteritems(): failed = "" - if not fpr is None: + if fpr is not None: if speak: print("Checking %s… " % mail, end="") key = localring.get(fpr, None) # On vérifie qu'on possède la clé… - if not key is None: + if key is not None: # …qu'elle correspond au mail… if any(["<%s>" % (mail,) in u["uid"] for u in key["uids"]]): if speak: @@ -475,7 +475,7 @@ def check_keys(options, recipients=None, quiet=False): print("") if failed: log.warn("--> Fail on %s:%s\n--> %s" % (mail, fpr, failed)) - if not recipients is None: + if recipients is not None: # On cherche à savoir si on droppe ce recipient message = "Abandonner le chiffrement pour cette clé ? (Si vous la conservez, il est posible que gpg crashe)" if confirm(options, message, ('drop', fpr, mail)): @@ -642,7 +642,7 @@ def show_servers(options): def saveclipboard(restore=False, old_clipboard=None): """Enregistre le contenu du presse-papier. Le rétablit si ``restore=True``""" - if restore and old_clipboard == None: + if restore and old_clipboard is None: return act = '-in' if restore else '-out' proc = subprocess.Popen(['xclip', act, '-selection', 'clipboard'], @@ -700,15 +700,15 @@ def show_file(options): # Essaie de planquer le mot de passe for line in texte.split('\n'): - catchPass = None + catch_pass = None # On essaie de trouver le pass pour le cacher dans le clipboard # si ce n'est déjà fait et si c'est voulu if not is_hidden and options.clipboard: - catchPass = pass_regexp.match(line) - if catchPass != None: + catch_pass = pass_regexp.match(line) + if catch_pass is not None: is_hidden = True # On met le mdp dans le clipboard en mémorisant son ancien contenu - old_clipboard = clipboard(catchPass.group(1)) + old_clipboard = clipboard(catch_pass.group(1)) # Et donc on override l'affichage line = "[Le mot de passe a été mis dans le presse papier]" filtered += line + '\n' @@ -763,7 +763,7 @@ def edit_file(options): new_roles = options.roles # Cas du nouveau fichier - if not gotit and not "pas les droits" in value: + if not gotit and "pas les droits" not in value: nfile = True if not options.quiet: print("Fichier introuvable") @@ -827,7 +827,7 @@ C'est-à-dire pour les utilisateurs suivants :\n%s""" % ( if not options.quiet: print(message) else: - ntexte = texte if ntexte == None else ntexte + ntexte = texte if ntexte is None else ntexte success, message = put_password(options, new_roles, ntexte) print(message) @@ -888,7 +888,7 @@ def recrypt_files(options, strict=False): """ rechiffre_roles = options.roles _, my_roles_w = get_my_roles(options) - if rechiffre_roles == None: + if rechiffre_roles is None: # Sans précisions, on prend tous les roles qu'on peut rechiffre_roles = my_roles_w @@ -958,7 +958,7 @@ def parse_roles(options, cast=False): strroles = options.roles allroles = all_roles(options) _, my_roles_w = get_my_roles(options) - if strroles == None: + if strroles is 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() @@ -976,7 +976,7 @@ def parse_roles(options, cast=False): 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 options.fname is None: log.warn("You need to provide a filename with this command") if not options.quiet: parser.print_help() -- GitLab From de7f9408f7ccb6e16b60a10b2ddbd86f7a2e7baf Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 21:34:18 +0200 Subject: [PATCH 19/19] Use Python secret builtin --- client.py | 76 ++++++++++++++++++++++++++----------------------------- tox.ini | 1 - 2 files changed, 36 insertions(+), 41 deletions(-) diff --git a/client.py b/client.py index 5ba7735..db3dd5c 100755 --- a/client.py +++ b/client.py @@ -13,16 +13,14 @@ import subprocess import json import tempfile import os -import atexit import argparse import re -import random -import string import time import datetime import copy import logging from configparser import ConfigParser +from secrets import token_urlsafe # Import a SSH client from paramiko.client import SSHClient @@ -108,7 +106,7 @@ def gpg(options, command, args=None): def _parse_timestamp(string, canbenone=False): """Interprète ``string`` comme un timestamp depuis l'Epoch.""" - if string == u'' and canbenone: + if string == '' and canbenone: return None return datetime.datetime(*time.localtime(int(string))[:7]) @@ -116,14 +114,14 @@ def _parse_timestamp(string, canbenone=False): def _parse_pub(data): """Interprète une ligne ``pub:``""" d = { - u'trustletter': data[1], - u'length': int(data[2]), - u'algorithm': int(data[3]), - u'longid': data[4], - u'signdate': _parse_timestamp(data[5]), - u'expiredate': _parse_timestamp(data[6], canbenone=True), - u'ownertrustletter': data[8], - u'capabilities': data[11], + 'trustletter': data[1], + 'length': int(data[2]), + 'algorithm': int(data[3]), + 'longid': data[4], + 'signdate': _parse_timestamp(data[5]), + 'expiredate': _parse_timestamp(data[6], canbenone=True), + 'ownertrustletter': data[8], + 'capabilities': data[11], } return d @@ -131,10 +129,10 @@ def _parse_pub(data): def _parse_uid(data): """Interprète une ligne ``uid:``""" d = { - u'trustletter': data[1], - u'signdate': _parse_timestamp(data[5], canbenone=True), - u'hash': data[7], - u'uid': data[9], + 'trustletter': data[1], + 'signdate': _parse_timestamp(data[5], canbenone=True), + 'hash': data[7], + 'uid': data[9], } return d @@ -142,7 +140,7 @@ def _parse_uid(data): def _parse_fpr(data): """Interprète une ligne ``fpr:``""" d = { - u'fpr': data[9], + 'fpr': data[9], } return d @@ -150,23 +148,23 @@ def _parse_fpr(data): def _parse_sub(data): """Interprète une ligne ``sub:``""" d = { - u'trustletter': data[1], - u'length': int(data[2]), - u'algorithm': int(data[3]), - u'longid': data[4], - u'signdate': _parse_timestamp(data[5]), - u'expiredate': _parse_timestamp(data[6], canbenone=True), - u'capabilities': data[11], + 'trustletter': data[1], + 'length': int(data[2]), + 'algorithm': int(data[3]), + 'longid': data[4], + 'signdate': _parse_timestamp(data[5]), + 'expiredate': _parse_timestamp(data[6], canbenone=True), + 'capabilities': data[11], } return d #: Functions to parse the recognized fields GPG_PARSERS = { - u'pub': _parse_pub, - u'uid': _parse_uid, - u'fpr': _parse_fpr, - u'sub': _parse_sub, + 'pub': _parse_pub, + 'uid': _parse_uid, + 'fpr': _parse_fpr, + 'sub': _parse_sub, } @@ -386,12 +384,11 @@ def get_my_roles(options): return (my_roles, my_roles_w) -def gen_password(): - """Génère un mot de passe aléatoire""" - random.seed(datetime.datetime.now().microsecond) - chars = string.letters + string.digits + '/=+*' - length = 15 - return u''.join([random.choice(chars) for _ in range(length)]) +def gen_password(length=15): + """ + Generate a random password + """ + return token_urlsafe(length) ###### # Local commands @@ -578,7 +575,6 @@ def editor(texte, annotations=""): # Avoid syntax hilight with ".txt". Would be nice to have some colorscheme # for annotations ... f = tempfile.NamedTemporaryFile(suffix='.txt') - atexit.register(f.close) if annotations: annotations = "# " + annotations.replace("\n", "\n# ") # Usually, there is already an ending newline in a text document @@ -593,7 +589,7 @@ def editor(texte, annotations=""): f.seek(0) ntexte = f.read().decode("utf-8", errors='ignore') f.close() - ntexte = u'\n'.join( + ntexte = '\n'.join( filter(lambda l: not l.startswith('#'), ntexte.split('\n'))) return ntexte @@ -818,9 +814,9 @@ C'est-à-dire pour les utilisateurs suivants :\n%s""" % ( ntexte = editor(texte, annotations) - if ((not nfile and ntexte in [u'', texte] # pas nouveau, vidé ou pas modifié + if ((not nfile and ntexte in ['', texte] # pas nouveau, vidé ou pas modifié and set(new_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. + or (nfile and ntexte == '')): # ou alors on a créé un fichier vide. message = "Pas de modification à enregistrer.\n" message += "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 += "Si c'est un nouveau fichier, vous avez tenté de le créer vide." @@ -846,7 +842,7 @@ def confirm(options, text, remember_key=None): if remember_key in _remember_dict: return _remember_dict[remember_key] while True: - out = input((text + u' (o/n)')).lower() + out = input((text + ' (o/n)')).lower() if out == 'o': res = True break @@ -863,7 +859,7 @@ def confirm(options, text, remember_key=None): def remove_file(options): """Supprime un fichier""" fname = options.fname - if not confirm(options, u'Êtes-vous sûr de vouloir supprimer %s ?' % (fname,)): + if not confirm(options, 'Êtes-vous sûr de vouloir supprimer %s ?' % (fname,)): return message = rm_file(fname) print(message) diff --git a/tox.ini b/tox.ini index 277cfb4..54b7ac8 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,6 @@ commands = flake8 client.py [flake8] -ignore = D203, W503, E203 exclude = .tox, .git, -- GitLab