client.py 21 KB
Newer Older
Daniel Stan's avatar
Daniel Stan committed
1 2
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
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
Daniel Stan committed
10

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

Daniel Stan's avatar
Daniel Stan committed
13 14 15
import sys
import subprocess
import json
16 17 18
import tempfile
import os
import atexit
Daniel Stan's avatar
Daniel Stan committed
19
import argparse
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
Daniel Stan committed
38

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?$',
41 42
        flags=re.IGNORECASE)

Daniel Stan's avatar
Daniel Stan committed
43
## GPG Definitions
44
#: path gu binaire gpg
Daniel Stan's avatar
Daniel Stan committed
45
GPG = '/usr/bin/gpg'
46
#: paramètres à fournir à gpg en fonction de l'action désirée
Daniel Stan's avatar
Daniel Stan committed
47
GPG_ARGS = {
48 49 50 51
    'decrypt' : ['-d'],
    'encrypt' : ['--armor', '-es'],
    'fingerprint' : ['--fingerprint'],
    'receive-keys' : ['--recv-keys'],
Daniel Stan's avatar
Daniel Stan committed
52
    }
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),
                  }
64
#: Mode verbeux
Daniel Stan's avatar
Daniel Stan committed
65
VERB = False
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 demaner confirmation»
69 70 71 72
FORCED = False
#: Droits à définir sur le fichier en édition
NROLES = None
#: Serveur à interroger (peuplée à l'exécution)
73
SERVER = None
74

Daniel Stan's avatar
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
84
    else:
85
        stderr = subprocess.PIPE
86
        full_command.extend(['--debug-level=1'])
Daniel Stan's avatar
Daniel Stan committed
87 88 89
    proc = subprocess.Popen(full_command,
                            stdin = subprocess.PIPE,
                            stdout = subprocess.PIPE,
90
                            stderr = stderr,
Daniel Stan's avatar
Daniel Stan committed
91
                            close_fds = True)
Daniel Stan's avatar
Daniel Stan committed
92
    if not VERB:
93
        proc.stderr.close()
Daniel Stan's avatar
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):
104
        if self.val == None:
105 106 107
            self.val = self.f()
        return self.val

108

Daniel Stan's avatar
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
Daniel Stan committed
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
    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())

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

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

146
@simple_memoize
Daniel Stan's avatar
Daniel Stan committed
147 148 149 150 151 152 153 154 155 156 157
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,
158 159
                                                'contents' : contents})

Daniel Stan's avatar
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():
166
    """Retourne la liste des rôles de l'utilisateur"""
Daniel Stan's avatar
Daniel Stan committed
167
    allr = all_roles()
168
    return filter(lambda role: SERVER['user'] in allr[role], allr.keys())
Daniel Stan's avatar
Daniel Stan committed
169

170
def gen_password():
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
Daniel Stan committed
177 178 179 180 181
######
## Local commands

def update_keys():
    """Met à jour les clés existantes"""
182
    
Daniel Stan's avatar
Daniel Stan committed
183
    keys = all_keys()
184
    
Daniel Stan's avatar
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
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 193 194 195 196 197
    keys = all_keys()
    gpg = gnupg.GPG(gnupghome='~/.gnupg')
    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
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):
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
Daniel Stan committed
240 241
def encrypt(roles, contents):
    """Chiffre le contenu pour les roles donnés"""
242
    
Daniel Stan's avatar
Daniel Stan committed
243
    allkeys = all_keys()
244
    recipients = get_recipients_of_roles(roles)
Daniel Stan's avatar
Daniel Stan committed
245
    
246
    fpr_recipients = []
Daniel Stan's avatar
Daniel Stan committed
247
    for recipient in recipients:
248 249 250 251
        fpr = allkeys[recipient][1]
        if fpr:
            fpr_recipients.append("-r")
            fpr_recipients.append(fpr)
252
    
253
    stdin, stdout = gpg("encrypt", fpr_recipients)
254
    stdin.write(contents.encode("utf-8"))
Daniel Stan's avatar
Daniel Stan committed
255
    stdin.close()
256
    out = stdout.read().decode("utf-8")
257
    if out == '':
Vincent Le gallic's avatar
Vincent Le gallic committed
258 259
        if not QUIET:
            print(u"Échec de chiffrement".encode("utf-8"))
260 261 262
        return None
    else:
        return out
Daniel Stan's avatar
Daniel Stan committed
263 264 265 266

def decrypt(contents):
    """Déchiffre le contenu"""
    stdin, stdout = gpg("decrypt")
267
    stdin.write(contents.encode("utf-8"))
Daniel Stan's avatar
Daniel Stan committed
268
    stdin.close()
269
    return stdout.read().decode("utf-8")
Daniel Stan's avatar
Daniel Stan committed
270 271 272 273 274

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
275 276
    if NROLES != None:
        roles = NROLES
277
        if VERB:
Vincent Le gallic's avatar
Vincent Le gallic committed
278
            print(u"Pas de nouveaux rôles".encode("utf-8"))
279 280 281 282
    if enc_pwd <> None:
        return put_file(name, roles, enc_pwd)
    else:
        return False
Daniel Stan's avatar
Daniel Stan committed
283 284 285

def get_password(name):
    """Récupère le mot de passe donné par name"""
286 287
    remotefile = get_file(name)
    return decrypt(remotefile['contents'])
Daniel Stan's avatar
Daniel Stan committed
288

289
######
290 291
## Interface

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

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

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

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

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


def show_file(fname):
369
    """Affiche le contenu d'un fichier"""
370 371
    value = get_file(fname)
    if value == False:
Vincent Le gallic's avatar
Vincent Le gallic committed
372
        print(u"Fichier introuvable".encode("utf-8"))
373
        return
374
    (sin, sout) = gpg('decrypt')
375
    sin.write(value['contents'].encode("utf-8"))
376
    sin.close()
377 378
    texte = sout.read().decode("utf-8")
    ntexte = u""
379 380 381 382 383
    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:
384
            hidden = True
385 386 387 388 389
            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
390 391
    raw = u"Fichier %s:\n\n%s-----\nVisible par: %s\n" % (fname, ntexte, ','.join(value['roles']))
    out.write(raw.encode("utf-8"))
392
    out.close()
393
    os.waitpid(proc.pid, 0)
394

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

425 426
    annotations += u"""Ce fichier sera chiffré pour les rôles suivants :\n%s\n
C'est-à-dire pour les utilisateurs suivants :\n%s""" % (
427 428 429 430 431
           ', '.join(value['roles']),
           '\n'.join(' %s' % rec for rec in get_dest_of_roles(value['roles']))
        )
        
    ntexte = editor(texte, annotations)
432 433

    if ntexte == None and not nfile and NROLES == None:
Vincent Le gallic's avatar
Vincent Le gallic committed
434
        print(u"Pas de modifications effectuées".encode("utf-8"))
435
    else:
Daniel Stan's avatar
Daniel Stan committed
436
        ntexte = texte if ntexte == None else ntexte
437
        if put_password(fname, value['roles'], ntexte):
Vincent Le gallic's avatar
Vincent Le gallic committed
438
            print(u"Modifications enregistrées".encode("utf-8"))
439
        else:
Vincent Le gallic's avatar
Vincent Le gallic committed
440
            print(u"Erreur lors de l'enregistrement (avez-vous les droits suffisants ?)".encode("utf-8"))
Daniel Stan's avatar
Daniel Stan committed
441 442

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

def remove_file(fname):
453
    """Supprime un fichier"""
454
    if not confirm((u'Êtes-vous sûr de vouloir supprimer %s ?' % fname).encode("utf-8")):
Daniel Stan's avatar
Daniel Stan committed
455
        return
456
    if rm_file(fname):
Vincent Le gallic's avatar
Vincent Le gallic committed
457
        print(u"Suppression effectuée".encode("utf-8"))
458
    else:
Vincent Le gallic's avatar
Vincent Le gallic committed
459
        print(u"Erreur de suppression (avez-vous les droits ?)".encode("utf-8"))
460

Daniel Stan's avatar
Daniel Stan committed
461 462

def my_check_keys():
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():
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():
472
    """Rechiffre les fichiers"""
473
    roles = None
Daniel Stan's avatar
Daniel Stan committed
474 475 476
    my_roles = get_my_roles()
    if roles == None:
        # On ne conserve que les rôles qui finissent par -w
477
        roles = [ r[:-2] for r in my_roles if r.endswith('-w')]
Daniel Stan's avatar
Daniel Stan committed
478 479 480
    if type(roles) != list:
        roles = [roles]

481
    for (fname, froles) in all_files().iteritems():
Daniel Stan's avatar
Daniel Stan committed
482 483
        if set(roles).intersection(froles) == set([]):
            continue
Vincent Le gallic's avatar
Vincent Le gallic committed
484
        print((u"Rechiffrement de %s" % fname).encode("utf-8"))
485
        put_password(fname, froles, get_password(fname))
Daniel Stan's avatar
Daniel Stan committed
486 487

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

if __name__ == "__main__":
Daniel Stan's avatar
Daniel Stan committed
514
    parser = argparse.ArgumentParser(description="trousseau crans")
515
    parser.add_argument('-s', '--server', default='default',
516 517
        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
518
        help="Mode verbeux")
519 520
    parser.add_argument('-q', '--quiet', action='store_true', default=False,
        help="Mode silencieux. Cache les message d'erreurs (override --verbose).")
521
    parser.add_argument('-c', '--clipboard', action='store_true', default=None,
Daniel Stan's avatar
Daniel Stan committed
522
        help="Stocker le mot de passe dans le presse papier")
523
    parser.add_argument('--no-clip', '--noclip', '--noclipboard', action='store_false', default=None,
524 525
        dest='clipboard',
        help="Ne PAS stocker le mot de passe dans le presse papier")
526 527
    parser.add_argument('-f', '--force', action='store_true', default=False,
        help="Ne pas demander confirmation")
Daniel Stan's avatar
Daniel Stan committed
528 529 530

    # Actions possibles
    action_grp = parser.add_mutually_exclusive_group(required=False)
531 532
    action_grp.add_argument('-e', '--edit', action='store_const', dest='action',
        default=show_file, const=edit_file,
Daniel Stan's avatar
Daniel Stan committed
533
        help="Editer (ou créer)")
534 535 536 537 538 539 540 541
    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
542
        help="Lister les fichiers")
543 544
    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
545
        help="Vérifier les clés")
546 547
    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
548
        help="Mettre à jour les clés")
549 550 551 552 553 554
    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")
555 556
    action_grp.add_argument('--recrypt-files', action='store_const', dest='action',
        default=show_file, const=recrypt_files,
557 558 559
        help="Rechiffrer les mots de passe")

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