Skip to content
Snippets Groups Projects
server.py 5.13 KiB
Newer Older
Daniel STAN's avatar
Daniel STAN committed
#!/usr/bin/env python
# -*- encoding: utf-8 -*-

"""Serveur pour cranspasswords"""
Daniel STAN's avatar
Daniel STAN committed

import glob
import os
import pwd
import sys
import json
import smtplib
Daniel STAN's avatar
Daniel STAN committed
import datetime
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
Daniel STAN's avatar
Daniel STAN committed

from serverconfig import READONLY, CRANSP_MAIL, DEST_MAIL, KEYS, ROLES, STORE
Daniel STAN's avatar
Daniel STAN committed
MYUID = pwd.getpwuid(os.getuid())[0]
if MYUID == 'root':
    MYUID = os.environ['SUDO_USER']

def validate(roles, mode='r'):
    """Vérifie que l'appelant appartient bien aux roles précisés
Daniel STAN's avatar
Daniel STAN committed
    Si mode mode='w', recherche un rôle en écriture
    """
Daniel STAN's avatar
Daniel STAN committed
    for role in roles:
        if mode == 'w':
            role += '-w'
Daniel STAN's avatar
Daniel STAN committed
        if ROLES.has_key(role) and MYUID in ROLES[role]:
Daniel STAN's avatar
Daniel STAN committed
            return True
    return False

def getpath(filename, backup=False):
    """Récupère le chemin du fichier ``filename``"""
    return os.path.join(STORE, '%s.%s' % (filename, 'bak' if backup else 'json'))
Daniel STAN's avatar
Daniel STAN committed

def writefile(filename, contents):
    """Écrit le fichier avec les bons droits UNIX"""
Daniel STAN's avatar
Daniel STAN committed
    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 usernames et les (mail, fingerprint) correspondants"""
Daniel STAN's avatar
Daniel STAN committed
    return KEYS

def listfiles():
    """Liste les fichiers dans l'espace de stockage, et les roles qui peuvent y accéder"""
    os.chdir(STORE)
Daniel STAN's avatar
Daniel STAN committed
    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``"""
Daniel STAN's avatar
Daniel STAN committed
    filepath = getpath(filename)
    try:
        obj = json.loads(open(filepath).read())
        if not validate(obj['roles']):
	        return False
        return obj
Daniel STAN's avatar
Daniel STAN committed
    except IOError:
        return False
     

def putfile(filename):
    """Écrit le fichier ``filename`` avec les données reçues sur stdin."""
Daniel STAN's avatar
Daniel STAN committed
    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
Daniel STAN's avatar
Daniel STAN committed
    try:
        old = getfile(filename)
        oldroles = old['roles']
Daniel STAN's avatar
Daniel STAN committed
    except TypeError:
Daniel STAN's avatar
Daniel STAN committed
        old = "[Création du fichier]"
Daniel STAN's avatar
Daniel STAN committed
        pass
    else:
Daniel STAN's avatar
Daniel STAN committed
        if not validate(oldroles,'w'):
Daniel STAN's avatar
Daniel STAN committed
            return False
    
    notification("Modification de %s" % filename,
        "Le fichier %s a été modifié par %s." % (filename, MYUID),
        filename, old)
Daniel STAN's avatar
Daniel STAN committed
    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:
        old = getfile(filename)
        roles = old['roles']
Daniel STAN's avatar
Daniel STAN committed
    except TypeError:
        return True
    else:
Daniel STAN's avatar
Daniel STAN committed
        if validate(roles,'w'):
            notification("Suppression de %s" % filename,\
                "Le fichier %s a été supprimé par %s." %\
Daniel STAN's avatar
Daniel STAN committed
                (filename,MYUID),filename,old)
Daniel STAN's avatar
Daniel STAN committed
            os.remove(getpath(filename))
        else:
            return False
    return True

def backup(fname, old):
    """Backupe l'ancienne version du fichier"""
    back = open(getpath(fname, backup=True), 'a')
Daniel STAN's avatar
Daniel STAN committed
    back.write(json.dumps(old))
    back.write('\n')
    back.write('* %s: %s\n' % (str(datetime.datetime.now()),corps)) 
    back.close()
    
def notification(subject, corps, fname, old):
    """Envoie par mail une notification de changement de fichier"""
    conn = smtplib.SMTP('localhost')
    frommail = CRANSP_MAIL
    tomail = DEST_MAIL
    msg = MIMEMultipart(_charset="utf-8")
    msg['Subject'] = subject
    msg['X-Mailer'] = "cranspasswords"
    msg['From'] = CRANSP_MAIL
    msg['To'] = DEST_MAIL
    msg.preamble = "cranspasswords report"
Daniel STAN's avatar
Daniel STAN committed
    info = MIMEText(corps + 
        "\nLa version précédente a été sauvegardée." +
Daniel STAN's avatar
Daniel STAN committed
        #"\nCi-joint l'ancien fichier." +
        "\n\n-- \nCranspasswords.py",_charset="utf-8")
    msg.attach(info)
    #old = MIMEText(old)
    #old.add_header('Content-Disposition', 'attachment', filename=fname) 
Daniel STAN's avatar
Daniel STAN committed
    #msg.attach(str(old))
    conn.sendmail(frommail,tomail,msg.as_string())
    conn.quit()

WRITE_COMMANDS = ["putfile", "rmfile"]

Daniel STAN's avatar
Daniel STAN committed
if __name__ == "__main__":
    argv = sys.argv[1:]
    if len(argv) not in [1, 2]:
        sys.exit(1)
    command = argv[0]
    if READONLY and command in WRITE_COMMANDS:
        raise IOError("Ce serveur est read-only.")
Daniel STAN's avatar
Daniel STAN committed
    filename = None
    try:
        filename = argv[1]
    except IndexError:
        pass
Daniel STAN's avatar
Daniel STAN committed
    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)