Skip to content
Snippets Groups Projects
cranspasswords.py 12.9 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
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
CLIPBOARD = False # Par défaut, place-t-on le mdp dans le presse-papier ?
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")
            email_recipients.append(email)

    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
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("less",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():
    print """Liste des roles disponibles""" 
    for role in all_roles().keys():
        if role.endswith('-w'): continue
        print " * " + role 

def clipboard(texte):
    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
    proc = subprocess.Popen("less",stdin=subprocess.PIPE,shell=True)
    out = proc.stdin
    out.write("Fichier %s:\n\n" % fname)
Daniel STAN's avatar
Daniel STAN committed
    (sin,sout) = gpg('decrypt')
    sin.write(value['contents'])
    sin.close()
    if CLIPBOARD:    # Ça ne va pas plaire à tout le monde
        texte = sout.read()
Daniel STAN's avatar
Daniel STAN committed
        lines = texte.split('\n')
        for line in lines:
            if line.startswith('pass:'):
                out.write(clipboard(line[5:].strip(' \t\r\n')) + '\n')
            else:
                out.write(line+'\n')
    else:  # Si pas de presse papier, on fait passer ça dans un less
        out.write(sout.read())
    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:
        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()

def update_role(roles=None):
    """ 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:
        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)):
            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=False,
        help="Stocker le mot de passe dans le presse papier")
    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:])
    VERB = parsed.verbose
Daniel STAN's avatar
Daniel STAN committed
    CLIPBOARD = parsed.clipboard
    FORCED = parsed.force
    NROLES = parse_roles(parsed.roles)
    SERVER = config.servers[parsed.server]
Daniel STAN's avatar
Daniel STAN committed

    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)
Daniel STAN's avatar
Daniel STAN committed