Skip to content
Snippets Groups Projects
cranspasswords.py 14.2 KiB
Newer Older
Daniel STAN's avatar
Daniel STAN committed
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""cranspasswords: gestion des mots de passe du Cr@ns"""

import sys
import subprocess
import json
Daniel STAN's avatar
Daniel STAN committed
import tempfile
import os
import atexit
Daniel STAN's avatar
Daniel STAN committed
import argparse
import re
try:
    import clientconfig as config
except ImportError:
    print "Read the README"
    sys.exit(1)
Daniel STAN's avatar
Daniel STAN committed

## Password pattern in files:
PASS = re.compile('[\t ]*pass(?:word)?[\t ]*:[\t ]*(.*)\r?\n?$', \
        flags=re.IGNORECASE)

Daniel STAN's avatar
Daniel STAN committed
######
## GPG Definitions

GPG = '/usr/bin/gpg'
GPG_ARGS = {
    'decrypt': ['-d'],
    'encrypt': ['--armor', '-es'],
    'fingerprint': ['--fingerprint'],
    'receive-keys': ['--recv-keys'],
    }

Daniel STAN's avatar
Daniel STAN committed
DEBUG = False
VERB = False
Daniel STAN's avatar
Daniel STAN committed
# Par défaut, place-t-on le mdp dans le presse-papier ?
CLIPBOARD = bool(os.getenv('DISPLAY')) and os.path.exists('/usr/bin/xclip')
Daniel STAN's avatar
Daniel STAN committed
FORCED = False #Mode interactif qui demande confirmation
NROLES = None     # Droits à définir sur le fichier en édition
Daniel STAN's avatar
Daniel STAN committed
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
    if VERB:
Daniel STAN's avatar
Daniel STAN committed
        stderr=sys.stderr
    else:
        stderr=subprocess.PIPE
        full_command.extend(['--debug-level=1'])
    #print full_command
Daniel STAN's avatar
Daniel STAN committed
    proc = subprocess.Popen(full_command,
                            stdin = subprocess.PIPE,
                            stdout = subprocess.PIPE,
Daniel STAN's avatar
Daniel STAN committed
                            stderr = stderr,
Daniel STAN's avatar
Daniel STAN committed
                            close_fds = True)
Daniel STAN's avatar
Daniel STAN committed
    if not VERB:
Daniel STAN's avatar
Daniel STAN committed
        proc.stderr.close()
Daniel STAN's avatar
Daniel STAN committed
    return proc.stdin, proc.stdout

######
## Remote commands


def ssh(command, arg = None):
    """Lance ssh avec les arguments donnés. Renvoie son entrée
    standard et sa sortie standard."""
    full_command = list(SERVER['server_cmd'])
Daniel STAN's avatar
Daniel STAN committed
    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())

def all_keys():
    """Récupère les clés du serveur distant"""
    return remote_command("listkeys")

def all_roles():
    """Récupère les roles du serveur distant"""
    return remote_command("listroles")

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,
                                                'contents': contents})
def rm_file(filename):
    """Supprime le fichier sur le serveur distant"""
    return remote_command("rmfile", filename)

Daniel STAN's avatar
Daniel STAN committed
def get_my_roles():
    """Retoure la liste des rôles perso"""
    allr = all_roles()
    return filter(lambda role: SERVER['user'] in allr[role],allr.keys())
Daniel STAN's avatar
Daniel STAN committed
######
## Local commands

def update_keys():
    """Met à jour les clés existantes"""

    keys = all_keys()

    _, stdout = gpg("receive-keys", [key for _, key in keys.values() if key])
    return stdout.read()

def check_keys():
    """Vérifie les clés existantes"""

    keys = all_keys()

    for mail, key in keys.values():
        if key:
            _, stdout = gpg("fingerprint", [key])
Daniel STAN's avatar
Daniel STAN committed
            if VERB:   print "Checking %s" % mail
Daniel STAN's avatar
Daniel STAN committed
            if str("<%s>" % mail.lower()) not in stdout.read().lower():
Daniel STAN's avatar
Daniel STAN committed
                if VERB:   print "-->Fail on %s" % mail
Daniel STAN's avatar
Daniel STAN committed
                break
    else:
        return True
    return False

def encrypt(roles, contents):
    """Chiffre le contenu pour les roles donnés"""

    recipients = set()
    allroles = all_roles()
    allkeys = all_keys()
    
    email_recipients = []
    for role in roles:
        for recipient in allroles[role]:
            recipients.add(recipient)
    for recipient in recipients:
        email, key = allkeys[recipient]
        if key:
            email_recipients.append("-r")
Daniel STAN's avatar
Daniel STAN committed

    stdin, stdout = gpg("encrypt", email_recipients)
    stdin.write(contents)
    stdin.close()
Daniel STAN's avatar
Daniel STAN committed
    out = stdout.read()
    if out == '':
Daniel STAN's avatar
Daniel STAN committed
        if VERB: print "Échec de chiffrement"
Daniel STAN's avatar
Daniel STAN committed
        return None
    else:
        return out
Daniel STAN's avatar
Daniel STAN committed

def decrypt(contents):
    """Déchiffre le contenu"""
    stdin, stdout = gpg("decrypt")
    stdin.write(contents)
    stdin.close()
    return stdout.read()

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
    if NROLES != None:
        roles = NROLES
        if VERB:
            print "Pas de nouveaux rôles"
Daniel STAN's avatar
Daniel STAN committed
    if enc_pwd <> None:
        return put_file(name, roles, enc_pwd)
    else:
        return False
Daniel STAN's avatar
Daniel STAN committed

def get_password(name):
    """Récupère le mot de passe donné par name"""
    remotefile = get_file(name)
    return decrypt(remotefile['contents'])

Daniel STAN's avatar
Daniel STAN committed
## Interface

def editor(texte):
    """ Lance $EDITOR sur texte"""
    f = tempfile.NamedTemporaryFile()
    atexit.register(f.close)
    f.write(texte)
    f.flush()
    proc = subprocess.Popen(os.getenv('EDITOR') + ' ' + f.name,shell=True)
    os.waitpid(proc.pid,0)
    f.seek(0)
    ntexte = f.read()
    f.close()
    return texte <> ntexte and ntexte or None

def show_files():
    proc = subprocess.Popen("cat",stdin=subprocess.PIPE,shell=True)
    out = proc.stdin
    out.write("""Liste des fichiers disponibles\n""" )
Daniel STAN's avatar
Daniel STAN committed
    my_roles = get_my_roles()
    for (fname,froles) in all_files().iteritems():
        access = set(my_roles).intersection(froles) != set([])
        out.write(" %s %s (%s)\n" % ((access and '+' or '-'),fname,", ".join(froles)))
    out.write("""--Mes roles: %s\n""" % \
        ", ".join(my_roles))
    
    out.close()
    os.waitpid(proc.pid,0)
Daniel STAN's avatar
Daniel STAN committed

def show_roles():
Daniel STAN's avatar
Daniel STAN committed
    print """Liste des roles disponibles"""
Daniel STAN's avatar
Daniel STAN committed
    for role in all_roles().keys():
        if role.endswith('-w'): continue
        print " * " + role 

old_clipboard = None
def saveclipboard(restore=False):
    global old_clipboard
    if restore and old_clipboard == None:
        return
    act = '-in' if restore else '-out'
    proc =subprocess.Popen(['xclip',act,'-selection','clipboard'],\
        stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=sys.stderr)
    if not restore:
        old_clipboard = proc.stdout.read()
    else:
        raw_input("Appuyez sur Entrée pour récupérer le contenu précédent du presse papier.")
        proc.stdin.write(old_clipboard)
    proc.stdin.close()
    proc.stdout.close()

Daniel STAN's avatar
Daniel STAN committed
def clipboard(texte):
    saveclipboard()
Daniel STAN's avatar
Daniel STAN committed
    proc =subprocess.Popen(['xclip','-selection','clipboard'],\
        stdin=subprocess.PIPE,stdout=sys.stdout,stderr=sys.stderr)
    proc.stdin.write(texte)
    proc.stdin.close()
    return "[Le mot de passe a été mis dans le presse papier]"
Daniel STAN's avatar
Daniel STAN committed


def show_file(fname):
    value = get_file(fname)
    if value == False:
        print "Fichier introuvable"
        return
Daniel STAN's avatar
Daniel STAN committed
    (sin,sout) = gpg('decrypt')
    sin.write(value['contents'])
    sin.close()
    texte = sout.read()
    ntexte = ""
    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:
            hidden=True
            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
    out.write("Fichier %s:\n\n" % fname)
    out.write(ntexte)
    out.write("-----\n")
    out.write("Visible par: %s\n" % ','.join(value['roles']))
    out.close()
    os.waitpid(proc.pid, 0)
Daniel STAN's avatar
Daniel STAN committed
        
def edit_file(fname):
    value = get_file(fname)
Daniel STAN's avatar
Daniel STAN committed
    nfile = False
Daniel STAN's avatar
Daniel STAN committed
    if value == False:
Daniel STAN's avatar
Daniel STAN committed
        nfile = True
        print "Fichier introuvable"
        if not confirm("Créer fichier ?"):
            return
        texte = ""
        roles = get_my_roles()
        # Par défaut les roles d'un fichier sont ceux en écriture de son
        # créateur
        roles = [ r[:-2] for r in roles if r.endswith('-w') ]
        if roles == []:
            print "Vous ne possédez aucun rôle en écriture ! Abandon."
            return
        value = {'roles':roles}
Daniel STAN's avatar
Daniel STAN committed
    else:
        (sin,sout) = gpg('decrypt')
        sin.write(value['contents'])
        sin.close()
        texte = sout.read()
Daniel STAN's avatar
Daniel STAN committed
    ntexte = editor(texte)
    if ntexte == None and not nfile and NROLES == None:
Daniel STAN's avatar
Daniel STAN committed
        print "Pas de modifications effectuées"
    else:
Daniel STAN's avatar
Daniel STAN committed
        ntexte = texte if ntexte == None else ntexte
Daniel STAN's avatar
Daniel STAN committed
        if put_password(fname,value['roles'],ntexte):
            print "Modifications enregistrées"
        else:
            print "Erreur lors de l'enregistrement (avez-vous les droits suffisants ?)"
Daniel STAN's avatar
Daniel STAN committed

def confirm(text):
    if FORCED: return True
    while True:
        out = raw_input(text + ' (O/N)').lower()
        if out == 'o':
            return True
        elif out == 'n':
            return False

def remove_file(fname):
    if not confirm('Êtes-vous sûr de vouloir supprimer %s ?' % fname):
        return
    if rm_file(fname):
        print "Suppression achevée"
    else:
        print "Erreur de suppression (avez-vous les droits ?)"
    

def my_check_keys():
    check_keys() and "Base de clés ok" or "Erreurs dans la base"

def my_update_keys():
    print update_keys()

Daniel STAN's avatar
Daniel STAN committed
def update_role():
    roles = None
Daniel STAN's avatar
Daniel STAN committed
    """ Reencode les fichiers, si roles est fourni,
    contient une liste de rôles"""
    my_roles = get_my_roles()
    if roles == None:
        # On ne conserve que les rôles qui finissent par -w
        roles = [ r[:-2] for r in my_roles if r.endswith('-w')]
Daniel STAN's avatar
Daniel STAN committed
    if type(roles) != list:
        roles = [roles]

    for (fname,froles) in all_files().iteritems():
        if set(roles).intersection(froles) == set([]):
            continue
        #if VERB:
        print "Reencodage de %s" % fname
        put_password(fname,froles,get_password(fname))

def parse_roles(strroles):
    if strroles == None: return None
    roles = all_roles()
    my_roles = filter(lambda r: SERVER['user'] in roles[r],roles.keys())
    my_roles_w = [ r[:-2] for r in my_roles if r.endswith('-w') ]
Daniel STAN's avatar
Daniel STAN committed
    ret = set()
    writable = False
    for role in strroles.split(','):
        if role not in roles.keys():
            print("Le rôle %s n'existe pas !" % role)
            return False
        if role.endswith('-w'):
            print("Le rôle %s ne devrait pas être utilisé ! (utilisez %s)"
                % (role,role[:-2]))
            return False
        writable = writable or role in my_roles_w
        ret.add(role)
    
    if not FORCED and not writable:
Daniel STAN's avatar
Daniel STAN committed
        if not confirm("""Vous vous apprêtez à perdre vos droits d'écritures\
(role ne contient pas %s) sur ce fichier, continuer ?""" % \
            ", ".join(my_roles_w)):
Daniel STAN's avatar
Daniel STAN committed
            return False
    return list(ret)
Daniel STAN's avatar
Daniel STAN committed

if __name__ == "__main__":
Daniel STAN's avatar
Daniel STAN committed
    parser = argparse.ArgumentParser(description="trousseau crans")
    parser.add_argument('--server',default='default',
        help='Utilisation d\'un serveur alternatif (test, etc)')
Daniel STAN's avatar
Daniel STAN committed
    parser.add_argument('-v','--verbose',action='store_true',default=False,
        help="Mode verbeux")
    parser.add_argument('-c','--clipboard',action='store_true',default=None,
Daniel STAN's avatar
Daniel STAN committed
        help="Stocker le mot de passe dans le presse papier")
    parser.add_argument('--noclipboard',action='store_false',default=None,
        dest='clipboard',
        help="Ne PAS stocker le mot de passe dans le presse papier")
Daniel STAN's avatar
Daniel STAN committed
    parser.add_argument('-f','--force',action='store_true',default=False,
        help="Forcer l'action")

    # Actions possibles
    action_grp = parser.add_mutually_exclusive_group(required=False)
    action_grp.add_argument('--edit',action='store_const',dest='action',
        default=show_file,const=edit_file,
Daniel STAN's avatar
Daniel STAN committed
        help="Editer (ou créer)")
Daniel STAN's avatar
Daniel STAN committed
    action_grp.add_argument('--view',action='store_const',dest='action',
        default=show_file,const=show_file,
        help="Voir")
    action_grp.add_argument('--remove',action='store_const',dest='action',
        default=show_file,const=remove_file,
        help="Effacer")
    action_grp.add_argument('-l','--list',action='store_const',dest='action',
        default=show_file,const=show_files,
        help="Lister les fichiers")
    action_grp.add_argument('--check-keys',action='store_const',dest='action',
        default=show_file,const=my_check_keys,
        help="Vérifier les clés")
    action_grp.add_argument('--update-keys',action='store_const',dest='action',
        default=show_file,const=my_update_keys,
        help="Mettre à jour les clés")
    action_grp.add_argument('--list-roles',action='store_const',dest='action',
        default=show_file,const=show_roles,
        help="Lister les rôles des gens")
    action_grp.add_argument('--recrypt-role',action='store_const',dest='action',
        default=show_file,const=update_role,
        help="Met à jour (reencode les roles)")

    parser.add_argument('--roles',nargs='?',default=None,
        help="liste des roles à affecter au fichier")
    parser.add_argument('fname',nargs='?',default=None,
        help="Nom du fichier à afficher")

    parsed = parser.parse_args(sys.argv[1:])
    SERVER = config.servers[parsed.server]
Daniel STAN's avatar
Daniel STAN committed
    VERB = parsed.verbose
    if parsed.clipboard != None:
        CLIPBOARD = parsed.clipboard
Daniel STAN's avatar
Daniel STAN committed
    FORCED = parsed.force
    NROLES = parse_roles(parsed.roles)

    if NROLES != False:
        if parsed.action.func_code.co_argcount == 0:
            parsed.action()
        elif parsed.fname == None:
            print("Vous devez fournir un nom de fichier avec cette commande")
            parser.print_help()
Daniel STAN's avatar
Daniel STAN committed
        else:
Daniel STAN's avatar
Daniel STAN committed
            parsed.action(parsed.fname)
    
    saveclipboard(restore=True)
Daniel STAN's avatar
Daniel STAN committed