From a3d0fee0dfdc8002385b0c4a9cc2acf999ada07d Mon Sep 17 00:00:00 2001 From: Daniel STAN Date: Wed, 23 May 2012 23:10:48 +0200 Subject: [PATCH] init --- cranspasswords-server.py | 177 +++++++++++++++++++++++++++++++++++++++ cranspasswords.py | 156 ++++++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100755 cranspasswords-server.py create mode 100755 cranspasswords.py diff --git a/cranspasswords-server.py b/cranspasswords-server.py new file mode 100755 index 0000000..3cd8750 --- /dev/null +++ b/cranspasswords-server.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +"""cranspasswords-server.py: Serveur pour cranspasswords""" + +import glob +import os +import pwd +import sys +import json + +MYUID = pwd.getpwuid(os.getuid())[0] +if MYUID == 'root': + MYUID = os.environ['SUDO_USER'] + +KEYS = { + "aza-vallina": ("Damien.Aza-Vallina@crans.org", None), + "dandrimont": ("nicolas.dandrimont@crans.org", "66475AAF"), + "nicolasd": (None, None), + "blockelet": ("blockelet@crans.org", "AF087A52"), + "chambart": ("pierre.chambart@crans.org", "F2530FCE"), + "dimino": ("jdimino@dptinfo.ens-cachan.fr", "2127F85A"), + "durand-gasselin": ("adg@crans.org", "8E96ACDA"), + "glondu": ("Stephane.Glondu@crans.org", "49881AD3"), + "huber": ("olivier.huber@crans.org", "E0DCF376"), + "lagorce": ("xavier.lagorce@crans.org", "0BF3708E"), + "parret-freaud": ("parret-freaud@crans.org", "7D980513"), + "tvincent": ("vincent.thomas@crans.org", "C5C4ACC0"), + } + +ROLES = { + "bureau": [ + "aza-vallina", + ], + "ca": [ + "aza-vallina", + "blockelet", + "durand-gasselin", + "lagorce", + ], + "rtc": [ + "dandrimont", + "nicolasd", + ], + "nounou": [ + "blockelet", + "chambart", + "dandrimont", + "dimino", + "durand-gasselin", + "glondu", + "huber", + "lagorce", + "parret-freaud", + "tvincent", + ], + } + +MYDIR = '/var/local/cranspasswords/' +STORE = MYDIR + 'store/' + +def validate(roles): + """Valide que l'appelant appartient bien aux roles précisés""" + for role in roles: + if MYUID in ROLES[role]: + return True + return False + +def getpath(filename): + """Récupère le chemin du fichier `filename'""" + return os.path.join(STORE, '%s.json' % filename) + +def writefile(filename, contents): + """Écrit le fichier de manière sécure""" + os.umask(0077) + f = open(filename, 'w') + f.write(contents) + f.close() + +def listroles(): + """Liste des roles existant et de leurs membres""" + return ROLES + +def listkeys(): + """Liste les uid et les clés correspondantes""" + return KEYS + +def listfiles(): + """Liste les fichiers dans l'espace de stockage, et les roles qui peuvent y accéder""" + os.chdir(STORE) + + filenames = glob.glob('*.json') + + files = {} + + for filename in filenames: + file_dict = json.loads(open(filename).read()) + files[filename[:-5]] = file_dict["roles"] + + return files + +def getfile(filename): + """Récupère le fichier `filename'""" + + filepath = getpath(filename) + try: + return json.loads(open(filepath).read()) + except IOError: + return False + + +def putfile(filename): + """Écrit le fichier `filename' avec les données reçues sur stdin.""" + + filepath = getpath(filename) + + stdin = sys.stdin.read() + parsed_stdin = json.loads(stdin) + + try: + roles = parsed_stdin['roles'] + contents = parsed_stdin['contents'] + except KeyError: + return False + + try: + oldroles = getfile(filename)['roles'] + except TypeError: + pass + else: + if not validate(oldroles): + return False + + writefile(filepath, json.dumps({'roles': roles, 'contents': contents})) + return True + +def rmfile(filename): + """Supprime le fichier filename après avoir vérifié les droits sur le fichier""" + try: + roles = getfile(filename)['roles'] + except TypeError: + return True + else: + if validate(roles): + os.remove(getpath(filename)) + else: + return False + return True + +if __name__ == "__main__": + argv = sys.argv[1:] + if len(argv) not in [1, 2]: + sys.exit(1) + command = argv[0] + filename = None + try: + filename = argv[1] + except IndexError: + pass + + if command == "listroles": + print json.dumps(listroles()) + elif command == "listkeys": + print json.dumps(listkeys()) + elif command == "listfiles": + print json.dumps(listfiles()) + else: + if not filename: + sys.exit(1) + if command == "getfile": + print json.dumps(getfile(filename)) + elif command == "putfile": + print json.dumps(putfile(filename)) + elif command == "rmfile": + print json.dumps(rmfile(filename)) + else: + sys.exit(1) + diff --git a/cranspasswords.py b/cranspasswords.py new file mode 100755 index 0000000..fa2e5ae --- /dev/null +++ b/cranspasswords.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +"""cranspasswords: gestion des mots de passe du Cr@ns""" + +import sys +import subprocess +import json + +###### +## GPG Definitions + +GPG = '/usr/bin/gpg' +GPG_ARGS = { + 'decrypt': ['-d'], + 'encrypt': ['--armor', '-es'], + 'fingerprint': ['--fingerprint'], + 'receive-keys': ['--recv-keys'], + } + +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) + proc = subprocess.Popen(full_command, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = sys.stderr, + close_fds = True) + return proc.stdin, proc.stdout + +###### +## Remote commands + +SSH = '/usr/bin/ssh' +SSH_HOST = 'localhost' +REMOTE_COMMAND = ['/home/nicolasd/cranspasswords-server.py'] + +def ssh(command, arg = None): + """Lance ssh avec les arguments donnés. Renvoie son entrée + standard et sa sortie standard.""" + full_command = [SSH, SSH_HOST] + full_command.extend(REMOTE_COMMAND) + 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) + +###### +## 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]) + if "<%s>" % mail.lower() not in stdout.read().lower(): + 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() + return stdout.read() + +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) + return put_file(name, roles, enc_pwd) + +def get_password(name): + """Récupère le mot de passe donné par name""" + remotefile = get_file(name) + return decrypt(remotefile['contents']) + +