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

"""Serveur pour cranspasswords"""
Daniel STAN's avatar
init  
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
init  
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
init  
Daniel STAN committed
18 19 20 21
MYUID = pwd.getpwuid(os.getuid())[0]
if MYUID == 'root':
    MYUID = os.environ['SUDO_USER']

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

Vincent Le gallic's avatar
Vincent Le gallic committed
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
init  
Daniel STAN committed
36 37

def writefile(filename, contents):
Vincent Le gallic's avatar
Vincent Le gallic committed
38
    """Écrit le fichier avec les bons droits UNIX"""
Daniel STAN's avatar
init  
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
init  
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():
Vincent Le gallic's avatar
Vincent Le gallic committed
49
    """Liste les usernames et les (mail, fingerprint) correspondants"""
Daniel STAN's avatar
init  
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)
Vincent Le gallic's avatar
Vincent Le gallic committed
55
    
Daniel STAN's avatar
init  
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):
Vincent Le gallic's avatar
Vincent Le gallic committed
64
    """Récupère le fichier ``filename``"""
Daniel STAN's avatar
init  
Daniel STAN committed
65 66
    filepath = getpath(filename)
    try:
root's avatar
root committed
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
init  
Daniel STAN committed
71
    except IOError:
72
        return [False, u"Le fichier %s n'existe pas." % filename]
Daniel STAN's avatar
init  
Daniel STAN committed
73 74 75
     

def putfile(filename):
Vincent Le gallic's avatar
Vincent Le gallic committed
76
    """Écrit le fichier ``filename`` avec les données reçues sur stdin."""
Daniel STAN's avatar
init  
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"]
Vincent Le gallic's avatar
Vincent Le gallic committed
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
init  
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
init  
Daniel STAN committed
99
    writefile(filepath, json.dumps({'roles': roles, 'contents': contents}))
100
    return [True, u"Modification effectuée."]
Daniel STAN's avatar
init  
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
init  
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
init  
Daniel STAN committed
116

117
def backup(corps, fname, old):
Vincent Le gallic's avatar
Vincent Le gallic committed
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

Vincent Le gallic's avatar
Vincent Le gallic committed
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
init  
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
init  
Daniel STAN committed
152 153 154 155 156
    filename = None
    try:
        filename = argv[1]
    except IndexError:
        pass
Vincent Le gallic's avatar
Vincent Le gallic committed
157
    
Daniel STAN's avatar
init  
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)