Skip to content
Snippets Groups Projects
server.py 5.47 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']):
Daniel STAN's avatar
Daniel STAN committed
    except IOError:
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:
    try:
        old = getfile(filename)
        oldroles = old['roles']
    except TypeError:
Vincent Le gallic's avatar
Vincent Le gallic committed
        old = u"[Création du fichier]"
Daniel STAN's avatar
Daniel STAN committed
        pass
    else:
        if not validate(oldroles,'w'):
            return False
        
        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}))
Daniel STAN's avatar
Daniel STAN committed

def rmfile(filename):
    """Supprime le fichier filename après avoir vérifié les droits sur le fichier"""
    try:
        old = getfile(filename)
        roles = old['roles']
    except TypeError:
        return True
Daniel STAN's avatar
Daniel STAN committed
    else:
        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))
        else:
            return False
    return True
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
Daniel STAN's avatar
Daniel STAN committed
    if command == "listroles":
Vincent Le gallic's avatar
Vincent Le gallic committed
        print(json.dumps(listroles()))
Daniel STAN's avatar
Daniel STAN committed
    elif command == "listkeys":
Vincent Le gallic's avatar
Vincent Le gallic committed
        print(json.dumps(listkeys()))
Daniel STAN's avatar
Daniel STAN committed
    elif command == "listfiles":
Vincent Le gallic's avatar
Vincent Le gallic committed
        print(json.dumps(listfiles()))
Daniel STAN's avatar
Daniel STAN committed
    else:
        if not filename:
            sys.exit(1)
        if command == "getfile":
Vincent Le gallic's avatar
Vincent Le gallic committed
            print(json.dumps(getfile(filename)))
Daniel STAN's avatar
Daniel STAN committed
        elif command == "putfile":
Vincent Le gallic's avatar
Vincent Le gallic committed
            print(json.dumps(putfile(filename)))
Daniel STAN's avatar
Daniel STAN committed
        elif command == "rmfile":
Vincent Le gallic's avatar
Vincent Le gallic committed
            print(json.dumps(rmfile(filename)))
Daniel STAN's avatar
Daniel STAN committed
        else:
            sys.exit(1)