client.py 23.1 KB
Newer Older
Daniel STAN's avatar
init  
Daniel STAN committed
1 2
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
Vincent Le gallic's avatar
Vincent Le gallic committed
3 4 5 6 7 8 9

"""Gestion centralisée des mots de passe avec chiffrement GPG

Copyright (C) 2010-2013 Cr@ns <roots@crans.org>
Authors : Daniel Stan <daniel.stan@crans.org>
          Vincent Le Gallic <legallic@crans.org>
"""
Daniel STAN's avatar
init  
Daniel STAN committed
10

Vincent Le gallic's avatar
Vincent Le gallic committed
11 12
from __future__ import print_function

Daniel STAN's avatar
init  
Daniel STAN committed
13 14 15
import sys
import subprocess
import json
Daniel STAN's avatar
Daniel STAN committed
16 17 18
import tempfile
import os
import atexit
Daniel STAN's avatar
Daniel STAN committed
19
import argparse
Daniel STAN's avatar
Daniel STAN committed
20
import re
21 22 23
import random
import string
import datetime
24 25 26 27 28
try:
    import gnupg #disponible seulement sous wheezy
except ImportError:
    if sys.stderr.isatty() and not any([opt in sys.argv for opt in ["-q", "--quiet"]]):
        sys.stderr.write(u"Package python-gnupg introuvable, vous ne pourrez pas vérifiez les clés.\n".encode("utf-8"))
29
try:
30 31 32
    # 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,)))
33 34
    import clientconfig as config
except ImportError:
35 36
    if sys.stderr.isatty() and not any([opt in sys.argv for opt in ["-q", "--quiet"]]):
        sys.stderr.write(u"Va lire le fichier README.\n".encode("utf-8"))
37
    sys.exit(1)
Daniel STAN's avatar
init  
Daniel STAN committed
38

Vincent Le gallic's avatar
Vincent Le gallic committed
39 40
#: pattern utilisé pour détecter la ligne contenant le mot de passe dans les fichiers
PASS = re.compile('[\t ]*pass(?:word)?[\t ]*:[\t ]*(.*)\r?\n?$',
Daniel STAN's avatar
Daniel STAN committed
41 42
        flags=re.IGNORECASE)

Daniel STAN's avatar
init  
Daniel STAN committed
43
## GPG Definitions
Vincent Le gallic's avatar
Vincent Le gallic committed
44
#: path gu binaire gpg
Daniel STAN's avatar
init  
Daniel STAN committed
45
GPG = '/usr/bin/gpg'
Vincent Le gallic's avatar
Vincent Le gallic committed
46
#: paramètres à fournir à gpg en fonction de l'action désirée
Daniel STAN's avatar
init  
Daniel STAN committed
47
GPG_ARGS = {
Vincent Le gallic's avatar
Vincent Le gallic committed
48 49 50 51
    'decrypt' : ['-d'],
    'encrypt' : ['--armor', '-es'],
    'fingerprint' : ['--fingerprint'],
    'receive-keys' : ['--recv-keys'],
Daniel STAN's avatar
init  
Daniel STAN committed
52
    }
Vincent Le gallic's avatar
Vincent Le gallic committed
53
#: map lettre de trustlevel -> (signification, faut-il faire confiance à la clé)
54 55 56 57 58 59 60 61 62 63
GPG_TRUSTLEVELS = {
                    u"-" : (u"inconnue", False),
                    u"n" : (u"nulle", False),
                    u"m" : (u"marginale", True),
                    u"f" : (u"entière", True),
                    u"u" : (u"ultime", True),
                    u"r" : (u"révoquée", False),
                    u"e" : (u"expirée", False),
                    u"q" : (u"/données insuffisantes/", False),
                  }
Vincent Le gallic's avatar
Vincent Le gallic committed
64
#: Mode verbeux
Daniel STAN's avatar
Daniel STAN committed
65
VERB = False
Vincent Le gallic's avatar
Vincent Le gallic committed
66
#: Par défaut, place-t-on le mdp dans le presse-papier ?
Daniel STAN's avatar
Daniel STAN committed
67
CLIPBOARD = bool(os.getenv('DISPLAY')) and os.path.exists('/usr/bin/xclip')
68
#: Mode «ne pas demander confirmation»
Vincent Le gallic's avatar
Vincent Le gallic committed
69 70
FORCED = False
#: Droits à définir sur le fichier en édition
71
NEWROLES = None
Vincent Le gallic's avatar
Vincent Le gallic committed
72
#: Serveur à interroger (peuplée à l'exécution)
73
SERVER = None
Daniel STAN's avatar
Daniel STAN committed
74

Daniel STAN's avatar
init  
Daniel STAN committed
75 76 77 78 79 80 81
def gpg(command, args = None):
    """Lance gpg pour la commande donnée avec les arguments
    donnés. Renvoie son entrée standard et sa sortie standard."""
    full_command = [GPG]
    full_command.extend(GPG_ARGS[command])
    if args:
        full_command.extend(args)
Daniel STAN's avatar
Daniel STAN committed
82
    if VERB:
83
        stderr = sys.stderr
Daniel STAN's avatar
Daniel STAN committed
84
    else:
85
        stderr = subprocess.PIPE
Daniel STAN's avatar
Daniel STAN committed
86
        full_command.extend(['--debug-level=1'])
Daniel STAN's avatar
init  
Daniel STAN committed
87 88 89
    proc = subprocess.Popen(full_command,
                            stdin = subprocess.PIPE,
                            stdout = subprocess.PIPE,
Daniel STAN's avatar
Daniel STAN committed
90
                            stderr = stderr,
Daniel STAN's avatar
init  
Daniel STAN committed
91
                            close_fds = True)
Daniel STAN's avatar
Daniel STAN committed
92
    if not VERB:
Daniel STAN's avatar
Daniel STAN committed
93
        proc.stderr.close()
Daniel STAN's avatar
init  
Daniel STAN committed
94 95
    return proc.stdin, proc.stdout

96 97 98 99 100 101 102 103

class simple_memoize(object):
    """ Memoization/Lazy """
    def __init__(self, f):
        self.f = f
        self.val = None

    def __call__(self):
Vincent Le gallic's avatar
Vincent Le gallic committed
104
        if self.val == None:
105 106 107
            self.val = self.f()
        return self.val

Vincent Le gallic's avatar
Vincent Le gallic committed
108

Daniel STAN's avatar
init  
Daniel STAN committed
109 110 111 112 113 114
######
## Remote commands

def ssh(command, arg = None):
    """Lance ssh avec les arguments donnés. Renvoie son entrée
    standard et sa sortie standard."""
115
    full_command = list(SERVER['server_cmd'])
Daniel STAN's avatar
init  
Daniel STAN committed
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
    full_command.append(command)
    if arg:
        full_command.append(arg)
    proc = subprocess.Popen(full_command,
                            stdin = subprocess.PIPE,
                            stdout = subprocess.PIPE,
                            stderr = sys.stderr,
                            close_fds = True)
    return proc.stdin, proc.stdout

def remote_command(command, arg = None, stdin_contents = None):
    """Exécute la commande distante, et retourne la sortie de cette
    commande"""
    
    sshin, sshout = ssh(command, arg)
131
    if not stdin_contents is None:
Daniel STAN's avatar
init  
Daniel STAN committed
132 133
        sshin.write(json.dumps(stdin_contents))
        sshin.close()
134 135
    raw_out = sshout.read()
    return json.loads(raw_out)
Daniel STAN's avatar
init  
Daniel STAN committed
136

137
@simple_memoize
Daniel STAN's avatar
init  
Daniel STAN committed
138 139 140 141
def all_keys():
    """Récupère les clés du serveur distant"""
    return remote_command("listkeys")

142
@simple_memoize
Daniel STAN's avatar
init  
Daniel STAN committed
143 144 145 146
def all_roles():
    """Récupère les roles du serveur distant"""
    return remote_command("listroles")

147
@simple_memoize
Daniel STAN's avatar
init  
Daniel STAN committed
148 149 150 151
def all_files():
    """Récupère les fichiers du serveur distant"""
    return remote_command("listfiles")

152 153 154
def get_files(filenames):
    """Récupère le contenu des fichiers distants"""
    return remote_command("getfiles", stdin_contents=filenames)
Daniel STAN's avatar
init  
Daniel STAN committed
155

156 157 158
def put_files(files):
    """Dépose les fichiers sur le serveur distant"""
    return remote_command("putfiles", stdin_contents=files)
Vincent Le gallic's avatar
Vincent Le gallic committed
159

Daniel STAN's avatar
init  
Daniel STAN committed
160 161 162 163
def rm_file(filename):
    """Supprime le fichier sur le serveur distant"""
    return remote_command("rmfile", filename)

164
@simple_memoize
Daniel STAN's avatar
Daniel STAN committed
165
def get_my_roles():
Vincent Le gallic's avatar
Vincent Le gallic committed
166
    """Retourne la liste des rôles de l'utilisateur"""
Daniel STAN's avatar
Daniel STAN committed
167
    allr = all_roles()
Vincent Le gallic's avatar
Vincent Le gallic committed
168
    return filter(lambda role: SERVER['user'] in allr[role], allr.keys())
Daniel STAN's avatar
Daniel STAN committed
169

170
def gen_password():
Vincent Le gallic's avatar
Vincent Le gallic committed
171
    """Génère un mot de passe aléatoire"""
172 173 174
    random.seed(datetime.datetime.now().microsecond)
    chars = string.letters + string.digits + '/=+*'
    length = 15
175
    return u''.join([random.choice(chars) for _ in xrange(length)])
176

Daniel STAN's avatar
init  
Daniel STAN committed
177 178 179 180 181
######
## Local commands

def update_keys():
    """Met à jour les clés existantes"""
Vincent Le gallic's avatar
Vincent Le gallic committed
182
    
Daniel STAN's avatar
init  
Daniel STAN committed
183
    keys = all_keys()
Vincent Le gallic's avatar
Vincent Le gallic committed
184
    
Daniel STAN's avatar
init  
Daniel STAN committed
185
    _, stdout = gpg("receive-keys", [key for _, key in keys.values() if key])
186
    return stdout.read().decode("utf-8")
Daniel STAN's avatar
init  
Daniel STAN committed
187 188 189

def check_keys():
    """Vérifie les clés existantes"""
190 191
    if VERB:
        print("M : l'uid correspond au mail du fingerprint\nC : confiance OK (inclu la vérification de non expiration).\n")
192
    keys = all_keys()
Vincent Le gallic's avatar
Vincent Le gallic committed
193
    gpg = gnupg.GPG()
194 195 196 197
    localkeys = gpg.list_keys()
    failed = False
    for (mail, fpr) in keys.values():
        if fpr:
198
            if VERB:
199
                print((u"Checking %s… " % (mail)).encode("utf-8"), end="")
200 201 202 203 204 205
            corresponds = [key for key in localkeys if key["fingerprint"] == fpr]
            # On vérifie qu'on possède la clé…
            if len(corresponds) == 1:
                correspond = corresponds[0]
                # …qu'elle correspond au mail…
                if mail.lower() in sum([re.findall("<(.*)>", uid.lower()) for uid in correspond["uids"]], []):
206 207
                    if VERB:
                        print("M ", end="")
208 209 210
                    meaning, trustvalue = GPG_TRUSTLEVELS[correspond["trust"]]
                    # … et qu'on lui fait confiance
                    if not trustvalue:
211
                        print((u"--> Fail on %s:%s\nLa confiance en la clé est : %s" % (fpr, mail, meaning,)).encode("utf-8"))
212
                        failed = True
213 214
                    elif VERB:
                        print("C ", end="")
215
                else:
Vincent Le gallic's avatar
Vincent Le gallic committed
216
                    print((u"--> Fail on %s:%s\n!! Le fingerprint et le mail ne correspondent pas !" % (fpr, mail)).encode("utf-8"))
217 218
                    failed = True
            else:
Vincent Le gallic's avatar
Vincent Le gallic committed
219
                print((u"--> Fail on %s:%s\nPas (ou trop) de clé avec ce fingerprint." % (fpr, mail)).encode("utf-8"))
220
                failed = True
221 222
            if VERB:
                print("")
223
    return not failed
Daniel STAN's avatar
init  
Daniel STAN committed
224

225 226 227 228 229 230 231 232 233 234
def get_recipients_of_roles(roles):
    """Renvoie les destinataires d'un rôle"""
    recipients = set()
    allroles = all_roles()
    for role in roles:
        for recipient in allroles[role]:
            recipients.add(recipient)
    return recipients

def get_dest_of_roles(roles):
Vincent Le gallic's avatar
Vincent Le gallic committed
235
    """Renvoie la liste des "username : mail (fingerprint)" """
236
    allkeys = all_keys()
237
    return [u"%s : %s (%s)" % (rec, allkeys[rec][0], allkeys[rec][1])
238
               for rec in get_recipients_of_roles(roles) if allkeys[rec][1]]
239

Daniel STAN's avatar
init  
Daniel STAN committed
240
def encrypt(roles, contents):
241
    """Chiffre ``contents`` pour les ``roles`` donnés"""
Daniel STAN's avatar
init  
Daniel STAN committed
242
    allkeys = all_keys()
243
    recipients = get_recipients_of_roles(roles)
Daniel STAN's avatar
init  
Daniel STAN committed
244
    
245
    fpr_recipients = []
Daniel STAN's avatar
init  
Daniel STAN committed
246
    for recipient in recipients:
247 248 249 250
        fpr = allkeys[recipient][1]
        if fpr:
            fpr_recipients.append("-r")
            fpr_recipients.append(fpr)
Vincent Le gallic's avatar
Vincent Le gallic committed
251
    
252
    stdin, stdout = gpg("encrypt", fpr_recipients)
253
    stdin.write(contents.encode("utf-8"))
Daniel STAN's avatar
init  
Daniel STAN committed
254
    stdin.close()
255
    out = stdout.read().decode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
256
    if out == '':
257
        return [False, u"Échec de chiffrement"]
Daniel STAN's avatar
Daniel STAN committed
258
    else:
259
        return [True, out]
Daniel STAN's avatar
init  
Daniel STAN committed
260 261 262 263

def decrypt(contents):
    """Déchiffre le contenu"""
    stdin, stdout = gpg("decrypt")
264
    stdin.write(contents.encode("utf-8"))
Daniel STAN's avatar
init  
Daniel STAN committed
265
    stdin.close()
266
    return stdout.read().decode("utf-8")
Daniel STAN's avatar
init  
Daniel STAN committed
267 268 269 270

def put_password(name, roles, contents):
    """Dépose le mot de passe après l'avoir chiffré pour les
    destinataires donnés"""
271
    success, enc_pwd_or_error = encrypt(roles, contents)
272 273
    if NEWROLES != None:
        roles = NEWROLES
Daniel STAN's avatar
Daniel STAN committed
274
        if VERB:
Vincent Le gallic's avatar
Vincent Le gallic committed
275
            print(u"Pas de nouveaux rôles".encode("utf-8"))
276 277
    if success:
        enc_pwd = enc_pwd_or_error
278
        return put_files([{'filename' : name, 'roles' : roles, 'contents' : enc_pwd}])[0]
Daniel STAN's avatar
Daniel STAN committed
279
    else:
280 281
        error = enc_pwd_or_error
        return [False, error]
Daniel STAN's avatar
init  
Daniel STAN committed
282 283 284

def get_password(name):
    """Récupère le mot de passe donné par name"""
285
    gotit, remotefile = get_files([name])[0]
286 287 288
    if gotit:
        remotefile = decrypt(remotefile['contents'])
    return [gotit, remotefile]
Daniel STAN's avatar
init  
Daniel STAN committed
289

Vincent Le gallic's avatar
Vincent Le gallic committed
290
######
Daniel STAN's avatar
Daniel STAN committed
291 292
## Interface

293
def editor(texte, annotations=u""):
294 295 296
    """ Lance $EDITOR sur texte.
    Renvoie le nouveau texte si des modifications ont été apportées, ou None
    """
Vincent Le gallic's avatar
Vincent Le gallic committed
297
    
298 299 300
    # Avoid syntax hilight with ".txt". Would be nice to have some colorscheme
    # for annotations ...
    f = tempfile.NamedTemporaryFile(suffix='.txt')
Daniel STAN's avatar
Daniel STAN committed
301
    atexit.register(f.close)
302 303 304
    if annotations:
        annotations = "# " + annotations.replace("\n", "\n# ")
    f.write((texte + "\n" + annotations).encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
305
    f.flush()
Daniel STAN's avatar
Daniel STAN committed
306
    proc = subprocess.Popen([os.getenv('EDITOR', '/usr/bin/editor'), f.name])
Vincent Le gallic's avatar
Vincent Le gallic committed
307
    os.waitpid(proc.pid, 0)
Daniel STAN's avatar
Daniel STAN committed
308
    f.seek(0)
309
    ntexte = f.read().decode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
310
    f.close()
311
    ntexte = u'\n'.join(filter(lambda l: not l.startswith('#'), ntexte.split('\n')))
312
    return ntexte
Daniel STAN's avatar
Daniel STAN committed
313 314

def show_files():
Vincent Le gallic's avatar
Vincent Le gallic committed
315
    """Affiche la liste des fichiers disponibles sur le serveur distant"""
Vincent Le gallic's avatar
Vincent Le gallic committed
316
    print(u"Liste des fichiers disponibles :".encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
317
    my_roles = get_my_roles()
318 319 320 321 322
    files = all_files()
    keys = files.keys()
    keys.sort()
    for fname in keys:
        froles = files[fname]
Daniel STAN's avatar
Daniel STAN committed
323
        access = set(my_roles).intersection(froles) != set([])
Vincent Le gallic's avatar
Vincent Le gallic committed
324 325
        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"))
326
    
Daniel STAN's avatar
Daniel STAN committed
327
def show_roles():
Vincent Le gallic's avatar
Vincent Le gallic committed
328
    """Affiche la liste des roles existants"""
Vincent Le gallic's avatar
Vincent Le gallic committed
329
    print(u"Liste des roles disponibles".encode("utf-8"))
330
    for (role, usernames) in all_roles().iteritems():
Vincent Le gallic's avatar
Vincent Le gallic committed
331
        if not role.endswith('-w'):
332
            print((u" * %s : %s" % (role, ", ".join(usernames))).encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
333

334
def show_servers():
Vincent Le gallic's avatar
Vincent Le gallic committed
335
    """Affiche la liste des serveurs disponibles"""
Vincent Le gallic's avatar
Vincent Le gallic committed
336
    print(u"Liste des serveurs disponibles".encode("utf-8"))
337
    for server in config.servers.keys():
Vincent Le gallic's avatar
Vincent Le gallic committed
338
        print((u" * " + server).encode("utf-8"))
339

340 341
old_clipboard = None
def saveclipboard(restore=False):
Vincent Le gallic's avatar
Vincent Le gallic committed
342
    """Enregistre le contenu du presse-papier. Le rétablit si ``restore=True``"""
343 344 345 346
    global old_clipboard
    if restore and old_clipboard == None:
        return
    act = '-in' if restore else '-out'
Vincent Le gallic's avatar
Vincent Le gallic committed
347 348
    proc = subprocess.Popen(['xclip', act, '-selection', 'clipboard'],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr)
349 350 351
    if not restore:
        old_clipboard = proc.stdout.read()
    else:
352
        raw_input(u"Appuyez sur Entrée pour récupérer le contenu précédent du presse papier.".encode("utf-8"))
353 354 355 356
        proc.stdin.write(old_clipboard)
    proc.stdin.close()
    proc.stdout.close()

Daniel STAN's avatar
Daniel STAN committed
357
def clipboard(texte):
Vincent Le gallic's avatar
Vincent Le gallic committed
358
    """Place ``texte`` dans le presse-papier en mémorisant l'ancien contenu."""
359
    saveclipboard()
Vincent Le gallic's avatar
Vincent Le gallic committed
360 361
    proc =subprocess.Popen(['xclip', '-selection', 'clipboard'],\
        stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr)
362
    proc.stdin.write(texte.encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
363
    proc.stdin.close()
364
    return u"[Le mot de passe a été mis dans le presse papier]"
Daniel STAN's avatar
Daniel STAN committed
365 366 367


def show_file(fname):
Vincent Le gallic's avatar
Vincent Le gallic committed
368
    """Affiche le contenu d'un fichier"""
369
    gotit, value = get_files([fname])[0]
370 371
    if not gotit:
        print(value.encode("utf-8")) # value contient le message d'erreur
Daniel STAN's avatar
Daniel STAN committed
372
        return
Vincent Le gallic's avatar
Vincent Le gallic committed
373
    (sin, sout) = gpg('decrypt')
374
    sin.write(value['contents'].encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
375
    sin.close()
376 377
    texte = sout.read().decode("utf-8")
    ntexte = u""
Daniel STAN's avatar
Daniel STAN committed
378 379 380 381 382
    hidden = False  # Est-ce que le mot de passe a été caché ?
    lines = texte.split('\n')
    for line in lines:
        catchPass = PASS.match(line)
        if catchPass != None and CLIPBOARD:
383
            hidden = True
Daniel STAN's avatar
Daniel STAN committed
384 385 386
            line = clipboard(catchPass.group(1))
        ntexte += line + '\n'
    showbin = "cat" if hidden else "less"
387
    proc = subprocess.Popen([showbin], stdin=subprocess.PIPE)
Daniel STAN's avatar
Daniel STAN committed
388
    out = proc.stdin
389 390
    raw = u"Fichier %s:\n\n%s-----\nVisible par: %s\n" % (fname, ntexte, ','.join(value['roles']))
    out.write(raw.encode("utf-8"))
391
    out.close()
Daniel STAN's avatar
Daniel STAN committed
392
    os.waitpid(proc.pid, 0)
393

Daniel STAN's avatar
Daniel STAN committed
394 395
        
def edit_file(fname):
Vincent Le gallic's avatar
Vincent Le gallic committed
396
    """Modifie/Crée un fichier"""
397
    gotit, value = get_files([fname])[0]
Daniel STAN's avatar
Daniel STAN committed
398
    nfile = False
399
    annotations = u""
400
    if not gotit and not "pas les droits" in value:
Daniel STAN's avatar
Daniel STAN committed
401
        nfile = True
Vincent Le gallic's avatar
Vincent Le gallic committed
402
        print(u"Fichier introuvable".encode("utf-8"))
Vincent Le gallic's avatar
Vincent Le gallic committed
403
        if not confirm(u"Créer fichier ?"):
Daniel STAN's avatar
Daniel STAN committed
404
            return
405
        annotations += u"""Ceci est un fichier initial contenant un mot de passe
Vincent Le gallic's avatar
Vincent Le gallic committed
406 407
aléatoire, pensez à rajouter une ligne "login: ${login}"
Enregistrez le fichier vide pour annuler.\n"""
408
        texte = u"pass: %s\n" % gen_password()
409 410 411
        roles = get_my_roles()
        # Par défaut les roles d'un fichier sont ceux en écriture de son
        # créateur
412
        roles = [ r[:-2] for r in roles if r.endswith('-w') ]
413
        if roles == []:
Vincent Le gallic's avatar
Vincent Le gallic committed
414
            print(u"Vous ne possédez aucun rôle en écriture ! Abandon.".encode("utf-8"))
415
            return
Vincent Le gallic's avatar
Vincent Le gallic committed
416
        value = {'roles' : roles}
417 418 419
    elif not gotit:
        print(value.encode("utf-8")) # value contient le message d'erreur
        return
Daniel STAN's avatar
Daniel STAN committed
420
    else:
Vincent Le gallic's avatar
Vincent Le gallic committed
421
        (sin, sout) = gpg('decrypt')
422
        sin.write(value['contents'].encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
423
        sin.close()
424
        texte = sout.read().decode("utf-8")
425 426
    # On récupère les nouveaux roles si ils ont été précisés, sinon on garde les mêmes
    value['roles'] = NEWROLES or value['roles']
427
    
Vincent Le gallic's avatar
Vincent Le gallic committed
428 429
    annotations += u"""Ce fichier sera chiffré pour les rôles suivants :\n%s\n
C'est-à-dire pour les utilisateurs suivants :\n%s""" % (
430 431 432 433 434
           ', '.join(value['roles']),
           '\n'.join(' %s' % rec for rec in get_dest_of_roles(value['roles']))
        )
        
    ntexte = editor(texte, annotations)
435
    
436
    if ((not nfile and ntexte in [u'', texte] and NEWROLES == None) or # Fichier existant vidé ou inchangé
437 438
        (nfile and ntexte == u'')):                                  # Nouveau fichier créé vide
        print(u"Pas de modification effectuée".encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
439
    else:
Daniel STAN's avatar
Daniel STAN committed
440
        ntexte = texte if ntexte == None else ntexte
441 442
        success, message = put_password(fname, value['roles'], ntexte)
        print(message.encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
443 444

def confirm(text):
Vincent Le gallic's avatar
Vincent Le gallic committed
445
    """Demande confirmation, sauf si on est mode ``FORCED``"""
Daniel STAN's avatar
Daniel STAN committed
446 447
    if FORCED: return True
    while True:
Vincent Le gallic's avatar
Vincent Le gallic committed
448
        out = raw_input((text + u' (O/N)').encode("utf-8")).lower()
Daniel STAN's avatar
Daniel STAN committed
449 450 451 452 453 454
        if out == 'o':
            return True
        elif out == 'n':
            return False

def remove_file(fname):
Vincent Le gallic's avatar
Vincent Le gallic committed
455
    """Supprime un fichier"""
456
    if not confirm(u'Êtes-vous sûr de vouloir supprimer %s ?' % fname):
Daniel STAN's avatar
Daniel STAN committed
457
        return
458 459
    message = rm_file(fname)
    print(message.encode("utf-8"))
Vincent Le gallic's avatar
Vincent Le gallic committed
460

Daniel STAN's avatar
Daniel STAN committed
461 462

def my_check_keys():
Vincent Le gallic's avatar
Vincent Le gallic committed
463
    """Vérifie les clés et affiche un message en fonction du résultat"""
Vincent Le gallic's avatar
Vincent Le gallic committed
464 465
    print(u"Vérification que les clés sont valides (uid correspondant au login) et de confiance.")
    print((check_keys() and u"Base de clés ok" or u"Erreurs dans la base").encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
466 467

def my_update_keys():
Vincent Le gallic's avatar
Vincent Le gallic committed
468
    """Met à jour les clés existantes et affiche le résultat"""
Vincent Le gallic's avatar
Vincent Le gallic committed
469
    print(update_keys().encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
470

471
def recrypt_files():
Vincent Le gallic's avatar
Vincent Le gallic committed
472
    """Rechiffre les fichiers"""
473 474
    # Ici, la signification de NEWROLES est : on ne veut rechiffrer que les fichiers qui ont au moins un de ces roles
    rechiffre_roles = NEWROLES
Daniel STAN's avatar
Daniel STAN committed
475
    my_roles = get_my_roles()
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509
    my_roles_w = [r for r in my_roles if r.endswith("-w")]
    if rechiffre_roles == None:
        # Sans précisions, on prend tous les roles qu'on peut
        rechiffre_roles = my_roles
    # On ne conserve que les rôles en écriture
    rechiffre_roles = [ r[:-2] for r in rechiffre_roles if r.endswith('-w')]
    
    # La liste des fichiers
    allfiles = all_files()
    # On ne demande que les fichiers dans lesquels on peut écrire
    # et qui ont au moins un role dans ``roles``
    askfiles = [filename for (filename, fileroles) in allfiles.iteritems()
                         if set(fileroles).intersection(roles) != set()
                         and set(fileroles).intersection(my_roles_w) != set()]
    files = get_files(askfiles)
    # Au cas où on aurait échoué à récupérer ne serait-ce qu'un de ces fichiers,
    # on affiche le message d'erreur correspondant et on abandonne.
    for (success, message) in files:
        if not success:
            print(message.encode("utf-8"))
            return
    # On rechiffre
    to_put = [{'filename' : f['filename'],
               'roles' : f['roles'],
               'contents' : encrypt(f['roles'], decrypt(f['contents']))}
              for f in files]
    if to_put:
        print((u"Rechiffrement de %s" % (", ".join([f['filename'] for f in to_put]))).encode("utf-8"))
        results = put_files(to_put)
        # On affiche les messages de retour
        for i in range(len(results)):
            print (u"%s : %s" % (to_put[i]['filename'], results[i][1]))
    else:
        print(u"Aucun fichier n'a besoin d'être rechiffré".encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
510 511

def parse_roles(strroles):
512 513
    """Interprête une liste de rôles fournie par l'utilisateur.
       Renvoie ``False`` si au moins un de ces rôles pose problème."""
Daniel STAN's avatar
Daniel STAN committed
514 515
    if strroles == None: return None
    roles = all_roles()
516
    my_roles = filter(lambda r: SERVER['user'] in roles[r], roles.keys())
517
    my_roles_w = [ r[:-2] for r in my_roles if r.endswith('-w') ]
Daniel STAN's avatar
Daniel STAN committed
518 519 520 521
    ret = set()
    writable = False
    for role in strroles.split(','):
        if role not in roles.keys():
Vincent Le gallic's avatar
Vincent Le gallic committed
522
            print((u"Le rôle %s n'existe pas !" % role).encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
523 524
            return False
        if role.endswith('-w'):
Vincent Le gallic's avatar
Vincent Le gallic committed
525
            print((u"Le rôle %s ne devrait pas être utilisé ! (utilisez %s)")
526
                   % (role, role[:-2])).encode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
527 528 529 530 531
            return False
        writable = writable or role in my_roles_w
        ret.add(role)
    
    if not FORCED and not writable:
532
        if not confirm(u"""Vous vous apprêtez à perdre vos droits d'écritures\
533
(ROLES ne contient pas %s) sur ce fichier, continuer ?""" %
Daniel STAN's avatar
Daniel STAN committed
534
            ", ".join(my_roles_w)):
Daniel STAN's avatar
Daniel STAN committed
535 536
            return False
    return list(ret)
Daniel STAN's avatar
Daniel STAN committed
537 538

if __name__ == "__main__":
Daniel STAN's avatar
Daniel STAN committed
539
    parser = argparse.ArgumentParser(description="trousseau crans")
540
    parser.add_argument('-s', '--server', default='default',
Vincent Le gallic's avatar
Vincent Le gallic committed
541 542
        help="Utilisation d'un serveur alternatif (test, backup, etc)")
    parser.add_argument('-v', '--verbose', action='store_true', default=False,
Daniel STAN's avatar
Daniel STAN committed
543
        help="Mode verbeux")
544 545
    parser.add_argument('-q', '--quiet', action='store_true', default=False,
        help="Mode silencieux. Cache les message d'erreurs (override --verbose).")
Vincent Le gallic's avatar
Vincent Le gallic committed
546
    parser.add_argument('-c', '--clipboard', action='store_true', default=None,
Daniel STAN's avatar
Daniel STAN committed
547
        help="Stocker le mot de passe dans le presse papier")
Vincent Le gallic's avatar
Vincent Le gallic committed
548
    parser.add_argument('--no-clip', '--noclip', '--noclipboard', action='store_false', default=None,
Daniel STAN's avatar
Daniel STAN committed
549 550
        dest='clipboard',
        help="Ne PAS stocker le mot de passe dans le presse papier")
Vincent Le gallic's avatar
Vincent Le gallic committed
551 552
    parser.add_argument('-f', '--force', action='store_true', default=False,
        help="Ne pas demander confirmation")
Daniel STAN's avatar
Daniel STAN committed
553 554 555

    # Actions possibles
    action_grp = parser.add_mutually_exclusive_group(required=False)
Vincent Le gallic's avatar
Vincent Le gallic committed
556 557
    action_grp.add_argument('-e', '--edit', action='store_const', dest='action',
        default=show_file, const=edit_file,
Daniel STAN's avatar
Daniel STAN committed
558
        help="Editer (ou créer)")
Vincent Le gallic's avatar
Vincent Le gallic committed
559 560 561 562 563 564 565 566
    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,
Daniel STAN's avatar
Daniel STAN committed
567
        help="Lister les fichiers")
Vincent Le gallic's avatar
Vincent Le gallic committed
568 569
    action_grp.add_argument('--check-keys', action='store_const', dest='action',
        default=show_file, const=my_check_keys,
Daniel STAN's avatar
Daniel STAN committed
570
        help="Vérifier les clés")
Vincent Le gallic's avatar
Vincent Le gallic committed
571 572
    action_grp.add_argument('--update-keys', action='store_const', dest='action',
        default=show_file, const=my_update_keys,
Daniel STAN's avatar
Daniel STAN committed
573
        help="Mettre à jour les clés")
Vincent Le gallic's avatar
Vincent Le gallic committed
574 575 576 577 578 579
    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")
580 581
    action_grp.add_argument('--recrypt-files', action='store_const', dest='action',
        default=show_file, const=recrypt_files,
582
        help="Rechiffrer les mots de passe. (Avec les mêmes rôles qu'avant, sert à rajouter un lecteur)")
Vincent Le gallic's avatar
Vincent Le gallic committed
583 584

    parser.add_argument('--roles', nargs='?', default=None,
585 586 587 588 589
        help="""Liste de roles (séparés par des virgules).
                Avec --edit, le fichier sera chiffré pour exactement ces roles
                (par défaut, tous vos rôles en écriture seront utilisés).
                Avec --recrypt-files, tous les fichiers ayant au moins un de ces roles (et pour lesquels vous avez le droit d'écriture) seront rechiffrés
                (par défaut, tous les fichiers pour lesquels vous avez les droits en écriture sont rechiffrés).""")
Vincent Le gallic's avatar
Vincent Le gallic committed
590
    parser.add_argument('fname', nargs='?', default=None,
Daniel STAN's avatar
Daniel STAN committed
591
        help="Nom du fichier à afficher")
Vincent Le gallic's avatar
Vincent Le gallic committed
592
    
Daniel STAN's avatar
Daniel STAN committed
593
    parsed = parser.parse_args(sys.argv[1:])
594
    SERVER = config.servers[parsed.server]
595 596
    QUIET = parsed.quiet
    VERB = parsed.verbose and not QUIET
Daniel STAN's avatar
Daniel STAN committed
597 598
    if parsed.clipboard != None:
        CLIPBOARD = parsed.clipboard
Daniel STAN's avatar
Daniel STAN committed
599
    FORCED = parsed.force
600
    NEWROLES = parse_roles(parsed.roles)
Vincent Le gallic's avatar
Vincent Le gallic committed
601
    
602
    if NEWROLES != False:
Daniel STAN's avatar
Daniel STAN committed
603 604 605
        if parsed.action.func_code.co_argcount == 0:
            parsed.action()
        elif parsed.fname == None:
606
            if not QUIET:
Vincent Le gallic's avatar
Vincent Le gallic committed
607
                print(u"Vous devez fournir un nom de fichier avec cette commande".encode("utf-8"))
608 609
                parser.print_help()
            sys.exit(1)
Daniel STAN's avatar
Daniel STAN committed
610
        else:
Daniel STAN's avatar
Daniel STAN committed
611
            parsed.action(parsed.fname)
612 613
    
    saveclipboard(restore=True)
Daniel STAN's avatar
init  
Daniel STAN committed
614