server.py 5.38 KB
Newer Older
Daniel STAN's avatar
Daniel STAN committed
1 2
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
3 4

"""Serveur pour cranspasswords"""
Daniel STAN's avatar
Daniel STAN committed
5 6 7 8 9 10

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

16
from serverconfig import READONLY, CRANSP_MAIL, DEST_MAIL, KEYS, ROLES, STORE
Daniel STAN's avatar
Daniel STAN committed
17

Daniel STAN's avatar
Daniel STAN committed
18 19 20 21
MYUID = pwd.getpwuid(os.getuid())[0]
if MYUID == 'root':
    MYUID = os.environ['SUDO_USER']

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

33 34 35
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
36 37

def writefile(filename, contents):
38
    """Écrit le fichier avec les bons droits UNIX"""
Daniel STAN's avatar
Daniel STAN committed
39 40
    os.umask(0077)
    f = open(filename, 'w')
Vincent Le gallic's avatar
Vincent Le gallic committed
41
    f.write(contents.encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
42 43 44 45 46 47 48
    f.close()

def listroles():
    """Liste des roles existant et de leurs membres"""
    return ROLES

def listkeys():
49
    """Liste les usernames et les (mail, fingerprint) correspondants"""
Daniel STAN's avatar
Daniel STAN committed
50 51 52 53 54
    return KEYS

def listfiles():
    """Liste les fichiers dans l'espace de stockage, et les roles qui peuvent y accéder"""
    os.chdir(STORE)
55
    
Daniel STAN's avatar
Daniel STAN committed
56 57 58 59 60 61 62 63
    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):
64
    """Récupère le fichier ``filename``"""
Daniel STAN's avatar
Daniel STAN committed
65 66
    filepath = getpath(filename)
    try:
67 68
        obj = json.loads(open(filepath).read())
        if not validate(obj['roles']):
69 70
	        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
71
    except IOError:
72
        return [False, u"Le fichier %s n'existe pas." % filename]
Daniel STAN's avatar
Daniel STAN committed
73 74 75
     

def putfile(filename):
76
    """Écrit le fichier ``filename`` avec les données reçues sur stdin."""
Daniel STAN's avatar
Daniel STAN committed
77 78 79 80 81 82 83
    filepath = getpath(filename)
    stdin = sys.stdin.read()
    parsed_stdin = json.loads(stdin)
    try:
        roles = parsed_stdin['roles']
        contents = parsed_stdin['contents']
    except KeyError:
84
        return [False, u"Entrée invalide"]
85
    
86 87
    gotit, old = getfile(filename)
    if not gotit:
Vincent Le gallic's avatar
Vincent Le gallic committed
88
        old = u"[Création du fichier]"
Daniel STAN's avatar
Daniel STAN committed
89 90
        pass
    else:
91 92 93 94 95 96 97
        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)
98
    
Daniel STAN's avatar
Daniel STAN committed
99
    writefile(filepath, json.dumps({'roles': roles, 'contents': contents}))
100
    return [True, u"Modification effectuée."]
Daniel STAN's avatar
Daniel STAN committed
101 102 103

def rmfile(filename):
    """Supprime le fichier filename après avoir vérifié les droits sur le fichier"""
104 105 106 107 108 109 110 111 112
    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
113
    else:
114 115
        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
116

117
def backup(corps, fname, old):
118 119
    """Backupe l'ancienne version du fichier"""
    back = open(getpath(fname, backup=True), 'a')
Daniel STAN's avatar
Daniel STAN committed
120 121
    back.write(json.dumps(old))
    back.write('\n')
122
    back.write((u'* %s: %s\n' % (str(datetime.datetime.now()), corps)).encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
123
    back.close()
124

125 126
def notification(subject, corps, fname, old):
    """Envoie par mail une notification de changement de fichier"""
127 128 129 130 131
    conn = smtplib.SMTP('localhost')
    frommail = CRANSP_MAIL
    tomail = DEST_MAIL
    msg = MIMEMultipart(_charset="utf-8")
    msg['Subject'] = subject
Vincent Le gallic's avatar
Vincent Le gallic committed
132
    msg['X-Mailer'] = u"cranspasswords"
133
    msg['From'] = CRANSP_MAIL
134
    msg['To'] = DEST_MAIL
Vincent Le gallic's avatar
Vincent Le gallic committed
135
    msg.preamble = u"cranspasswords report"
Daniel STAN's avatar
Daniel STAN committed
136
    info = MIMEText(corps + 
Vincent Le gallic's avatar
Vincent Le gallic committed
137 138
        u"\nLa version précédente a été sauvegardée." +
        u"\n\n-- \nCranspasswords.py", _charset="utf-8")
139
    msg.attach(info)
Vincent Le gallic's avatar
Vincent Le gallic committed
140
    conn.sendmail(frommail, tomail, msg.as_string())
141 142
    conn.quit()

143 144
WRITE_COMMANDS = ["putfile", "rmfile"]

Daniel STAN's avatar
Daniel STAN committed
145 146 147 148 149
if __name__ == "__main__":
    argv = sys.argv[1:]
    if len(argv) not in [1, 2]:
        sys.exit(1)
    command = argv[0]
150 151
    if READONLY and command in WRITE_COMMANDS:
        raise IOError("Ce serveur est read-only.")
Daniel STAN's avatar
Daniel STAN committed
152 153 154 155 156
    filename = None
    try:
        filename = argv[1]
    except IndexError:
        pass
157
    
Daniel STAN's avatar
Daniel STAN committed
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
    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)