client.py 20.5 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 11 12 13

import sys
import subprocess
import json
Daniel STAN's avatar
Daniel STAN committed
14 15 16
import tempfile
import os
import atexit
Daniel STAN's avatar
Daniel STAN committed
17
import argparse
Daniel STAN's avatar
Daniel STAN committed
18
import re
19 20 21
import random
import string
import datetime
22 23 24 25 26
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"))
27
try:
28 29 30
    # 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,)))
31 32
    import clientconfig as config
except ImportError:
33 34
    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"))
35
    sys.exit(1)
Daniel STAN's avatar
init  
Daniel STAN committed
36

Vincent Le gallic's avatar
Vincent Le gallic committed
37 38
#: 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
39 40
        flags=re.IGNORECASE)

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

Daniel STAN's avatar
init  
Daniel STAN committed
73 74 75 76 77 78 79
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
80
    if VERB:
81
        stderr = sys.stderr
Daniel STAN's avatar
Daniel STAN committed
82
    else:
83
        stderr = subprocess.PIPE
Daniel STAN's avatar
Daniel STAN committed
84
        full_command.extend(['--debug-level=1'])
Daniel STAN's avatar
init  
Daniel STAN committed
85 86 87
    proc = subprocess.Popen(full_command,
                            stdin = subprocess.PIPE,
                            stdout = subprocess.PIPE,
Daniel STAN's avatar
Daniel STAN committed
88
                            stderr = stderr,
Daniel STAN's avatar
init  
Daniel STAN committed
89
                            close_fds = True)
Daniel STAN's avatar
Daniel STAN committed
90
    if not VERB:
Daniel STAN's avatar
Daniel STAN committed
91
        proc.stderr.close()
Daniel STAN's avatar
init  
Daniel STAN committed
92 93
    return proc.stdin, proc.stdout

94 95 96 97 98 99 100 101

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
102
        if self.val == None:
103 104 105
            self.val = self.f()
        return self.val

Vincent Le gallic's avatar
Vincent Le gallic committed
106

Daniel STAN's avatar
init  
Daniel STAN committed
107 108 109 110 111 112
######
## Remote commands

def ssh(command, arg = None):
    """Lance ssh avec les arguments donnés. Renvoie son entrée
    standard et sa sortie standard."""
113
    full_command = list(SERVER['server_cmd'])
Daniel STAN's avatar
init  
Daniel STAN committed
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
    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)
    if stdin_contents:
        sshin.write(json.dumps(stdin_contents))
        sshin.close()
    return json.loads(sshout.read())

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

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

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

def get_file(filename):
    """Récupère le contenu du fichier distant"""
    return remote_command("getfile", filename)

def put_file(filename, roles, contents):
    """Dépose le fichier sur le serveur distant"""
    return remote_command("putfile", filename, {'roles': roles,
Vincent Le gallic's avatar
Vincent Le gallic committed
156 157
                                                'contents' : contents})

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

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

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

Daniel STAN's avatar
init  
Daniel STAN committed
175 176 177 178 179
######
## Local commands

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

def check_keys():
    """Vérifie les clés existantes"""
188 189 190 191 192 193
    keys = all_keys()
    gpg = gnupg.GPG(gnupghome='~/.gnupg')
    localkeys = gpg.list_keys()
    failed = False
    for (mail, fpr) in keys.values():
        if fpr:
194 195
            if VERB:
                print (u"Checking %s" % (mail)).encode("utf-8")
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
            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"]], []):
                    meaning, trustvalue = GPG_TRUSTLEVELS[correspond["trust"]]
                    # … et qu'on lui fait confiance
                    if not trustvalue:
                        print (u"--> Fail on %s:%s\nLa confiance en la clé est : %s" % (meaning,)).encode("utf-8")
                        failed = True
                else:
                    print (u"--> Fail on %s:%s\n!! Le fingerprint et le mail ne correspondent pas !" % (fpr, mail)).encode("utf-8")
                    failed = True
            else:
                print (u"--> Fail on %s:%s\nPas (ou trop) de clé avec ce fingerprint." % (fpr, mail)).encode("utf-8")
                failed = True
    return not failed
Daniel STAN's avatar
init  
Daniel STAN committed
214

215 216 217 218 219 220 221 222 223 224
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
225
    """Renvoie la liste des "username : mail (fingerprint)" """
226
    allkeys = all_keys()
227
    return [u"%s : %s (%s)" % (rec, allkeys[rec][0], allkeys[rec][1])
228
               for rec in get_recipients_of_roles(roles) if allkeys[rec][1]]
229

Daniel STAN's avatar
init  
Daniel STAN committed
230 231
def encrypt(roles, contents):
    """Chiffre le contenu pour les roles donnés"""
Vincent Le gallic's avatar
Vincent Le gallic committed
232
    
Daniel STAN's avatar
init  
Daniel STAN committed
233
    allkeys = all_keys()
234
    recipients = get_recipients_of_roles(roles)
Daniel STAN's avatar
init  
Daniel STAN committed
235
    
236
    fpr_recipients = []
Daniel STAN's avatar
init  
Daniel STAN committed
237
    for recipient in recipients:
238 239 240 241
        fpr = allkeys[recipient][1]
        if fpr:
            fpr_recipients.append("-r")
            fpr_recipients.append(fpr)
Vincent Le gallic's avatar
Vincent Le gallic committed
242
    
243
    stdin, stdout = gpg("encrypt", fpr_recipients)
244
    stdin.write(contents.encode("utf-8"))
Daniel STAN's avatar
init  
Daniel STAN committed
245
    stdin.close()
246
    out = stdout.read().decode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
247
    if out == '':
248 249
        if VERB:
            print u"Échec de chiffrement".encode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
250 251 252
        return None
    else:
        return out
Daniel STAN's avatar
init  
Daniel STAN committed
253 254 255 256

def decrypt(contents):
    """Déchiffre le contenu"""
    stdin, stdout = gpg("decrypt")
257
    stdin.write(contents.encode("utf-8"))
Daniel STAN's avatar
init  
Daniel STAN committed
258
    stdin.close()
259
    return stdout.read().decode("utf-8")
Daniel STAN's avatar
init  
Daniel STAN committed
260 261 262 263 264

def put_password(name, roles, contents):
    """Dépose le mot de passe après l'avoir chiffré pour les
    destinataires donnés"""
    enc_pwd = encrypt(roles, contents)
Daniel STAN's avatar
Daniel STAN committed
265 266
    if NROLES != None:
        roles = NROLES
Daniel STAN's avatar
Daniel STAN committed
267
        if VERB:
268
            print u"Pas de nouveaux rôles".encode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
269 270 271 272
    if enc_pwd <> None:
        return put_file(name, roles, enc_pwd)
    else:
        return False
Daniel STAN's avatar
init  
Daniel STAN committed
273 274 275

def get_password(name):
    """Récupère le mot de passe donné par name"""
276 277
    remotefile = get_file(name)
    return decrypt(remotefile['contents'])
Daniel STAN's avatar
init  
Daniel STAN committed
278

Vincent Le gallic's avatar
Vincent Le gallic committed
279
######
Daniel STAN's avatar
Daniel STAN committed
280 281
## Interface

282
def editor(texte, annotations=u""):
283 284 285
    """ 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
286
    
287 288 289
    # 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
290
    atexit.register(f.close)
291 292 293
    if annotations:
        annotations = "# " + annotations.replace("\n", "\n# ")
    f.write((texte + "\n" + annotations).encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
294
    f.flush()
Daniel STAN's avatar
Daniel STAN committed
295
    proc = subprocess.Popen([os.getenv('EDITOR', '/usr/bin/editor'), f.name])
Vincent Le gallic's avatar
Vincent Le gallic committed
296
    os.waitpid(proc.pid, 0)
Daniel STAN's avatar
Daniel STAN committed
297
    f.seek(0)
298
    ntexte = f.read().decode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
299
    f.close()
300
    ntexte = u'\n'.join(filter(lambda l: not l.startswith('#'), ntexte.split('\n')))
301 302 303
    if texte != ntexte:
        return ntexte
    return None
Daniel STAN's avatar
Daniel STAN committed
304 305

def show_files():
Vincent Le gallic's avatar
Vincent Le gallic committed
306
    """Affiche la liste des fichiers disponibles sur le serveur distant"""
307
    print u"Liste des fichiers disponibles :".encode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
308
    my_roles = get_my_roles()
309 310 311 312 313
    files = all_files()
    keys = files.keys()
    keys.sort()
    for fname in keys:
        froles = files[fname]
Daniel STAN's avatar
Daniel STAN committed
314
        access = set(my_roles).intersection(froles) != set([])
Vincent Le gallic's avatar
Vincent Le gallic committed
315 316
        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")
317
    
Daniel STAN's avatar
Daniel STAN committed
318
def show_roles():
Vincent Le gallic's avatar
Vincent Le gallic committed
319
    """Affiche la liste des roles existants"""
320
    print u"Liste des roles disponibles".encode("utf-8")
321
    for role in all_roles().keys():
Vincent Le gallic's avatar
Vincent Le gallic committed
322
        if not role.endswith('-w'):
323
            print (u" * " + role ).encode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
324

325
def show_servers():
Vincent Le gallic's avatar
Vincent Le gallic committed
326
    """Affiche la liste des serveurs disponibles"""
327
    print u"Liste des serveurs disponibles".encode("utf-8")
328
    for server in config.servers.keys():
329
        print (u" * " + server).encode("utf-8")
330

331 332
old_clipboard = None
def saveclipboard(restore=False):
Vincent Le gallic's avatar
Vincent Le gallic committed
333
    """Enregistre le contenu du presse-papier. Le rétablit si ``restore=True``"""
334 335 336 337
    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
338 339
    proc = subprocess.Popen(['xclip', act, '-selection', 'clipboard'],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr)
340 341 342
    if not restore:
        old_clipboard = proc.stdout.read()
    else:
343
        raw_input(u"Appuyez sur Entrée pour récupérer le contenu précédent du presse papier.".encode("utf-8"))
344 345 346 347
        proc.stdin.write(old_clipboard)
    proc.stdin.close()
    proc.stdout.close()

Daniel STAN's avatar
Daniel STAN committed
348
def clipboard(texte):
Vincent Le gallic's avatar
Vincent Le gallic committed
349
    """Place ``texte`` dans le presse-papier en mémorisant l'ancien contenu."""
350
    saveclipboard()
Vincent Le gallic's avatar
Vincent Le gallic committed
351 352
    proc =subprocess.Popen(['xclip', '-selection', 'clipboard'],\
        stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr)
353
    proc.stdin.write(texte.encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
354
    proc.stdin.close()
355
    return u"[Le mot de passe a été mis dans le presse papier]"
Daniel STAN's avatar
Daniel STAN committed
356 357 358


def show_file(fname):
Vincent Le gallic's avatar
Vincent Le gallic committed
359
    """Affiche le contenu d'un fichier"""
360 361 362
    value = get_file(fname)
    if value == False:
        print u"Fichier introuvable".encode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
363
        return
Vincent Le gallic's avatar
Vincent Le gallic committed
364
    (sin, sout) = gpg('decrypt')
365
    sin.write(value['contents'].encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
366
    sin.close()
367 368
    texte = sout.read().decode("utf-8")
    ntexte = u""
Daniel STAN's avatar
Daniel STAN committed
369 370 371 372 373
    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:
374
            hidden = True
Daniel STAN's avatar
Daniel STAN committed
375 376 377 378 379
            line = clipboard(catchPass.group(1))
        ntexte += line + '\n'
    showbin = "cat" if hidden else "less"
    proc = subprocess.Popen(showbin, stdin=subprocess.PIPE, shell=True)
    out = proc.stdin
380 381
    raw = u"Fichier %s:\n\n%s-----\nVisible par: %s\n" % (fname, ntexte, ','.join(value['roles']))
    out.write(raw.encode("utf-8"))
382
    out.close()
Daniel STAN's avatar
Daniel STAN committed
383
    os.waitpid(proc.pid, 0)
384

Daniel STAN's avatar
Daniel STAN committed
385 386
        
def edit_file(fname):
Vincent Le gallic's avatar
Vincent Le gallic committed
387
    """Modifie/Crée un fichier"""
388
    value = get_file(fname)
Daniel STAN's avatar
Daniel STAN committed
389
    nfile = False
390
    annotations = u""
391
    if value == False:
Daniel STAN's avatar
Daniel STAN committed
392
        nfile = True
393
        print u"Fichier introuvable".encode("utf-8")
Vincent Le gallic's avatar
Vincent Le gallic committed
394
        if not confirm(u"Créer fichier ?"):
Daniel STAN's avatar
Daniel STAN committed
395
            return
396
        annotations += u"""Ceci est un fichier initial contenant un mot de passe
Vincent Le gallic's avatar
Vincent Le gallic committed
397 398
aléatoire, pensez à rajouter une ligne "login: ${login}"
Enregistrez le fichier vide pour annuler.\n"""
399
        texte = u"pass: %s\n" % gen_password()
400 401 402
        roles = get_my_roles()
        # Par défaut les roles d'un fichier sont ceux en écriture de son
        # créateur
403
        roles = [ r[:-2] for r in roles if r.endswith('-w') ]
404
        if roles == []:
405
            print u"Vous ne possédez aucun rôle en écriture ! Abandon.".encode("utf-8")
406
            return
Vincent Le gallic's avatar
Vincent Le gallic committed
407
        value = {'roles' : roles}
Daniel STAN's avatar
Daniel STAN committed
408
    else:
Vincent Le gallic's avatar
Vincent Le gallic committed
409
        (sin, sout) = gpg('decrypt')
410
        sin.write(value['contents'].encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
411
        sin.close()
412
        texte = sout.read().decode("utf-8")
413
    value['roles'] = NROLES or value['roles']
414

Vincent Le gallic's avatar
Vincent Le gallic committed
415 416
    annotations += u"""Ce fichier sera chiffré pour les rôles suivants :\n%s\n
C'est-à-dire pour les utilisateurs suivants :\n%s""" % (
417 418 419 420 421
           ', '.join(value['roles']),
           '\n'.join(' %s' % rec for rec in get_dest_of_roles(value['roles']))
        )
        
    ntexte = editor(texte, annotations)
422 423 424

    if ntexte == None and not nfile and NROLES == None:
        print u"Pas de modifications effectuées".encode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
425
    else:
Daniel STAN's avatar
Daniel STAN committed
426
        ntexte = texte if ntexte == None else ntexte
427 428 429 430
        if put_password(fname, value['roles'], ntexte):
            print u"Modifications enregistrées".encode("utf-8")
        else:
            print u"Erreur lors de l'enregistrement (avez-vous les droits suffisants ?)".encode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
431 432

def confirm(text):
Vincent Le gallic's avatar
Vincent Le gallic committed
433
    """Demande confirmation, sauf si on est mode ``FORCED``"""
Daniel STAN's avatar
Daniel STAN committed
434 435
    if FORCED: return True
    while True:
Vincent Le gallic's avatar
Vincent Le gallic committed
436
        out = raw_input((text + u' (O/N)').encode("utf-8")).lower()
Daniel STAN's avatar
Daniel STAN committed
437 438 439 440 441 442
        if out == 'o':
            return True
        elif out == 'n':
            return False

def remove_file(fname):
Vincent Le gallic's avatar
Vincent Le gallic committed
443
    """Supprime un fichier"""
444
    if not confirm((u'Êtes-vous sûr de vouloir supprimer %s ?' % fname).encode("utf-8")):
Daniel STAN's avatar
Daniel STAN committed
445
        return
446 447 448 449
    if rm_file(fname):
        print u"Suppression effectuée".encode("utf-8")
    else:
        print u"Erreur de suppression (avez-vous les droits ?)".encode("utf-8")
Vincent Le gallic's avatar
Vincent Le gallic committed
450

Daniel STAN's avatar
Daniel STAN committed
451 452

def my_check_keys():
Vincent Le gallic's avatar
Vincent Le gallic committed
453
    """Vérifie les clés et affiche un message en fonction du résultat"""
454
    print u"Vérification que les clés sont valides (uid correspondant au login) et de confiance."
455
    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
456 457

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

461
def recrypt_files():
Vincent Le gallic's avatar
Vincent Le gallic committed
462
    """Rechiffre les fichiers"""
Daniel STAN's avatar
Daniel STAN committed
463
    roles = None
Daniel STAN's avatar
Daniel STAN committed
464 465 466
    my_roles = get_my_roles()
    if roles == None:
        # On ne conserve que les rôles qui finissent par -w
467
        roles = [ r[:-2] for r in my_roles if r.endswith('-w')]
Daniel STAN's avatar
Daniel STAN committed
468 469 470
    if type(roles) != list:
        roles = [roles]

Vincent Le gallic's avatar
Vincent Le gallic committed
471
    for (fname, froles) in all_files().iteritems():
Daniel STAN's avatar
Daniel STAN committed
472 473
        if set(roles).intersection(froles) == set([]):
            continue
474
        print (u"Rechiffrement de %s" % fname).encode("utf-8")
475
        put_password(fname, froles, get_password(fname))
Daniel STAN's avatar
Daniel STAN committed
476 477

def parse_roles(strroles):
Vincent Le gallic's avatar
Vincent Le gallic committed
478
    """Interprête une liste de rôles fournie par l'utilisateur"""
Daniel STAN's avatar
Daniel STAN committed
479 480
    if strroles == None: return None
    roles = all_roles()
481
    my_roles = filter(lambda r: SERVER['user'] in roles[r],roles.keys())
482
    my_roles_w = [ r[:-2] for r in my_roles if r.endswith('-w') ]
Daniel STAN's avatar
Daniel STAN committed
483 484 485 486
    ret = set()
    writable = False
    for role in strroles.split(','):
        if role not in roles.keys():
487
            print (u"Le rôle %s n'existe pas !" % role).encode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
488 489
            return False
        if role.endswith('-w'):
490
            print (u"Le rôle %s ne devrait pas être utilisé ! (utilisez %s)"
491
                   % (role, role[:-2])).encode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
492 493 494 495 496
            return False
        writable = writable or role in my_roles_w
        ret.add(role)
    
    if not FORCED and not writable:
497
        if not confirm(u"""Vous vous apprêtez à perdre vos droits d'écritures\
498
(ROLES ne contient pas %s) sur ce fichier, continuer ?""" %
Daniel STAN's avatar
Daniel STAN committed
499
            ", ".join(my_roles_w)):
Daniel STAN's avatar
Daniel STAN committed
500 501
            return False
    return list(ret)
Daniel STAN's avatar
Daniel STAN committed
502 503

if __name__ == "__main__":
Daniel STAN's avatar
Daniel STAN committed
504
    parser = argparse.ArgumentParser(description="trousseau crans")
505
    parser.add_argument('-s', '--server', default='default',
Vincent Le gallic's avatar
Vincent Le gallic committed
506 507
        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
508
        help="Mode verbeux")
509 510
    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
511
    parser.add_argument('-c', '--clipboard', action='store_true', default=None,
Daniel STAN's avatar
Daniel STAN committed
512
        help="Stocker le mot de passe dans le presse papier")
Vincent Le gallic's avatar
Vincent Le gallic committed
513
    parser.add_argument('--no-clip', '--noclip', '--noclipboard', action='store_false', default=None,
Daniel STAN's avatar
Daniel STAN committed
514 515
        dest='clipboard',
        help="Ne PAS stocker le mot de passe dans le presse papier")
Vincent Le gallic's avatar
Vincent Le gallic committed
516 517
    parser.add_argument('-f', '--force', action='store_true', default=False,
        help="Ne pas demander confirmation")
Daniel STAN's avatar
Daniel STAN committed
518 519 520

    # Actions possibles
    action_grp = parser.add_mutually_exclusive_group(required=False)
Vincent Le gallic's avatar
Vincent Le gallic committed
521 522
    action_grp.add_argument('-e', '--edit', action='store_const', dest='action',
        default=show_file, const=edit_file,
Daniel STAN's avatar
Daniel STAN committed
523
        help="Editer (ou créer)")
Vincent Le gallic's avatar
Vincent Le gallic committed
524 525 526 527 528 529 530 531
    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
532
        help="Lister les fichiers")
Vincent Le gallic's avatar
Vincent Le gallic committed
533 534
    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
535
        help="Vérifier les clés")
Vincent Le gallic's avatar
Vincent Le gallic committed
536 537
    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
538
        help="Mettre à jour les clés")
Vincent Le gallic's avatar
Vincent Le gallic committed
539 540 541 542 543 544
    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")
545 546
    action_grp.add_argument('--recrypt-files', action='store_const', dest='action',
        default=show_file, const=recrypt_files,
Vincent Le gallic's avatar
Vincent Le gallic committed
547 548 549
        help="Rechiffrer les mots de passe")

    parser.add_argument('--roles', nargs='?', default=None,
Daniel STAN's avatar
Daniel STAN committed
550
        help="liste des roles à affecter au fichier")
Vincent Le gallic's avatar
Vincent Le gallic committed
551
    parser.add_argument('fname', nargs='?', default=None,
Daniel STAN's avatar
Daniel STAN committed
552
        help="Nom du fichier à afficher")
Vincent Le gallic's avatar
Vincent Le gallic committed
553
    
Daniel STAN's avatar
Daniel STAN committed
554
    parsed = parser.parse_args(sys.argv[1:])
555
    SERVER = config.servers[parsed.server]
556 557
    QUIET = parsed.quiet
    VERB = parsed.verbose and not QUIET
Daniel STAN's avatar
Daniel STAN committed
558 559
    if parsed.clipboard != None:
        CLIPBOARD = parsed.clipboard
Daniel STAN's avatar
Daniel STAN committed
560 561
    FORCED = parsed.force
    NROLES = parse_roles(parsed.roles)
Vincent Le gallic's avatar
Vincent Le gallic committed
562
    
Daniel STAN's avatar
Daniel STAN committed
563 564 565 566
    if NROLES != False:
        if parsed.action.func_code.co_argcount == 0:
            parsed.action()
        elif parsed.fname == None:
567 568 569 570
            if not QUIET:
                print u"Vous devez fournir un nom de fichier avec cette commande".encode("utf-8")
                parser.print_help()
            sys.exit(1)
Daniel STAN's avatar
Daniel STAN committed
571
        else:
Daniel STAN's avatar
Daniel STAN committed
572
            parsed.action(parsed.fname)
573 574
    
    saveclipboard(restore=True)
Daniel STAN's avatar
init  
Daniel STAN committed
575