client.py 20.4 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
    sys.path.append("~/.config/%s/" % (config.cmd_name,))
29 30
    import clientconfig as config
except ImportError:
31 32
    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"))
33
    sys.exit(1)
Daniel STAN's avatar
init  
Daniel STAN committed
34

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

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

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

93 94 95 96 97 98 99 100

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

Vincent Le gallic's avatar
Vincent Le gallic committed
105

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

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

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

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

143
@simple_memoize
Daniel STAN's avatar
init  
Daniel STAN committed
144 145 146 147 148 149 150 151 152 153 154
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
155 156
                                                'contents' : contents})

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

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

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

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

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

def check_keys():
    """Vérifie les clés existantes"""
187 188 189 190 191 192
    keys = all_keys()
    gpg = gnupg.GPG(gnupghome='~/.gnupg')
    localkeys = gpg.list_keys()
    failed = False
    for (mail, fpr) in keys.values():
        if fpr:
193 194
            if VERB:
                print (u"Checking %s" % (mail)).encode("utf-8")
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
            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
213

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

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

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

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
264 265
    if NROLES != None:
        roles = NROLES
Daniel STAN's avatar
Daniel STAN committed
266
        if VERB:
267
            print u"Pas de nouveaux rôles".encode("utf-8")
Daniel STAN's avatar
Daniel STAN committed
268 269 270 271
    if enc_pwd <> None:
        return put_file(name, roles, enc_pwd)
    else:
        return False
Daniel STAN's avatar
init  
Daniel STAN committed
272 273 274

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

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

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

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

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

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

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


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

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

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

    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
424
    else:
Daniel STAN's avatar
Daniel STAN committed
425
        ntexte = texte if ntexte == None else ntexte
426 427 428 429
        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
430 431

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

def remove_file(fname):
Vincent Le gallic's avatar
Vincent Le gallic committed
442
    """Supprime un fichier"""
443
    if not confirm((u'Êtes-vous sûr de vouloir supprimer %s ?' % fname).encode("utf-8")):
Daniel STAN's avatar
Daniel STAN committed
444
        return
445 446 447 448
    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
449

Daniel STAN's avatar
Daniel STAN committed
450 451

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

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

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

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

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

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

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

    parser.add_argument('--roles', nargs='?', default=None,
Daniel STAN's avatar
Daniel STAN committed
549
        help="liste des roles à affecter au fichier")
Vincent Le gallic's avatar
Vincent Le gallic committed
550
    parser.add_argument('fname', nargs='?', default=None,
Daniel STAN's avatar
Daniel STAN committed
551
        help="Nom du fichier à afficher")
Vincent Le gallic's avatar
Vincent Le gallic committed
552
    
Daniel STAN's avatar
Daniel STAN committed
553
    parsed = parser.parse_args(sys.argv[1:])
554
    SERVER = config.servers[parsed.server]
555 556
    QUIET = parsed.quiet
    VERB = parsed.verbose and not QUIET
Daniel STAN's avatar
Daniel STAN committed
557 558
    if parsed.clipboard != None:
        CLIPBOARD = parsed.clipboard
Daniel STAN's avatar
Daniel STAN committed
559 560
    FORCED = parsed.force
    NROLES = parse_roles(parsed.roles)
Vincent Le gallic's avatar
Vincent Le gallic committed
561
    
Daniel STAN's avatar
Daniel STAN committed
562 563 564 565
    if NROLES != False:
        if parsed.action.func_code.co_argcount == 0:
            parsed.action()
        elif parsed.fname == None:
566 567 568 569
            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
570
        else:
Daniel STAN's avatar
Daniel STAN committed
571
            parsed.action(parsed.fname)
572 573
    
    saveclipboard(restore=True)
Daniel STAN's avatar
init  
Daniel STAN committed
574