client.py 20.6 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 demaner confirmation»
Vincent Le gallic's avatar
Vincent Le gallic committed
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
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 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
init  
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
init  
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
init  
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,
Vincent Le gallic's avatar
Vincent Le gallic committed
158 159
                                                'contents' : contents})

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

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

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

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

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

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

Vincent Le gallic's avatar
Vincent Le gallic committed
281
######
Daniel STAN's avatar
Daniel STAN committed
282 283
## Interface

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

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

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

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

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


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

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

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

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

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

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

Daniel STAN's avatar
Daniel STAN committed
453 454

def my_check_keys():
Vincent Le gallic's avatar
Vincent Le gallic committed
455
    """Vérifie les clés et affiche un message en fonction du résultat"""
Vincent Le gallic's avatar
Vincent Le gallic committed
456 457
    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
458 459

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

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

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

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

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

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

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