Skip to content
Snippets Groups Projects
server.py 5.77 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

Vincent Le gallic's avatar
Vincent Le gallic committed
from __future__ import print_function

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

# Même problème que pour le client, il faut bootstraper le nom de la commande
# Pour accéder à la config
cmd_name = os.path.split(sys.argv[0])[1].replace("-server", "")
sys.path.append("/etc/%s/" % (cmd_name,))
import serverconfig
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'
        if serverconfig.ROLES.has_key(role) and MYUID in serverconfig.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(serverconfig.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')
Vincent Le gallic's avatar
Vincent Le gallic committed
    f.write(contents.encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
    f.close()

def listroles():
    """Liste des roles existant et de leurs membres"""
    return serverconfig.ROLES
Daniel STAN's avatar
Daniel STAN committed

def listkeys():
    """Liste les usernames et les (mail, fingerprint) correspondants"""
    return serverconfig.KEYS
Daniel STAN's avatar
Daniel STAN committed

def listfiles():
    """Liste les fichiers dans l'espace de stockage, et les roles qui peuvent y accéder"""
    os.chdir(serverconfig.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, u"Vous n'avez pas les droits de lecture sur le fichier %s." % filename]
        return [True, obj]
Daniel STAN's avatar
Daniel STAN committed
    except IOError:
        return [False, u"Le fichier %s n'existe pas." % filename]
Daniel STAN's avatar
Daniel STAN committed
     

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, u"Entrée invalide"]
    gotit, old = getfile(filename)
    if not gotit:
Vincent Le gallic's avatar
Vincent Le gallic committed
        old = u"[Création du fichier]"
Daniel STAN's avatar
Daniel STAN committed
        pass
    else:
        oldroles = old['roles']
        if not validate(oldroles, 'w'):
            return [False, u"Vous n'avez pas le droit d'écriture sur %s." % filename]
    
    corps = u"Le fichier %s a été modifié par %s." % (filename, MYUID)
    backup(corps, filename, old)
    notification(u"Modification de %s" % filename, corps, filename, old)
Daniel STAN's avatar
Daniel STAN committed
    writefile(filepath, json.dumps({'roles': roles, 'contents': contents}))
    return [True, u"Modification effectuée."]
Daniel STAN's avatar
Daniel STAN committed

def rmfile(filename):
    """Supprime le fichier filename après avoir vérifié les droits sur le fichier"""
    gotit, old = getfile(filename)
    if not gotit:
        return old # contient le message d'erreur
    roles = old['roles']
    if validate(roles, 'w'):
        corps = u"Le fichier %s a été supprimé par %s." % (filename, MYUID)
        backup(corps, filename, old)
        notification(u"Suppression de %s" % filename, corps, filename, old)
        os.remove(getpath(filename))
Daniel STAN's avatar
Daniel STAN committed
    else:
        return u"Vous n'avez pas les droits d'écriture sur le fichier %s." % filename
    return u"Suppression effectuée"
Daniel STAN's avatar
Daniel STAN committed

def backup(corps, 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((u'* %s: %s\n' % (str(datetime.datetime.now()), corps)).encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
    back.close()
def notification(subject, corps, fname, old):
    """Envoie par mail une notification de changement de fichier"""
    conn = smtplib.SMTP('localhost')
    frommail = serverconfig.CRANSP_MAIL
    tomail = serverconfig.DEST_MAIL
    msg = MIMEMultipart(_charset="utf-8")
    msg['Subject'] = subject
    msg['X-Mailer'] = serverconfig.cmd_name.decode()
    msg['From'] = serverconfig.CRANSP_MAIL
    msg['To'] = serverconfig.DEST_MAIL
    msg.preamble = u"%s report" % (serverconfig.cmd_name.decode(),)
Daniel STAN's avatar
Daniel STAN committed
    info = MIMEText(corps + 
Vincent Le gallic's avatar
Vincent Le gallic committed
        u"\nLa version précédente a été sauvegardée." +
        u"\n\n-- \nCranspasswords.py", _charset="utf-8")
    msg.attach(info)
Vincent Le gallic's avatar
Vincent Le gallic committed
    conn.sendmail(frommail, tomail, msg.as_string())
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 serverconfig.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
    answer = None
Daniel STAN's avatar
Daniel STAN committed
    if command == "listroles":
        answer = listroles()
Daniel STAN's avatar
Daniel STAN committed
    elif command == "listkeys":
        answer = listkeys()
Daniel STAN's avatar
Daniel STAN committed
    elif command == "listfiles":
        answer = listfiles()
Daniel STAN's avatar
Daniel STAN committed
    else:
        if not filename:
            sys.exit(1)
        if command == "getfile":
            answer = getfile(filename)
Daniel STAN's avatar
Daniel STAN committed
        elif command == "putfile":
            answer = putfile(filename)
Daniel STAN's avatar
Daniel STAN committed
        elif command == "rmfile":
            answer = rmfile(filename)
Daniel STAN's avatar
Daniel STAN committed
        else:
            sys.exit(1)
    if not answer is None:
        print(json.dumps(answer))