server.py 6.9 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

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

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

Vincent Le gallic's avatar
Vincent Le gallic committed
18 19 20 21 22
# 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
23

Daniel STAN's avatar
init  
Daniel STAN committed
24 25 26 27
MYUID = pwd.getpwuid(os.getuid())[0]
if MYUID == 'root':
    MYUID = os.environ['SUDO_USER']

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

Vincent Le gallic's avatar
Vincent Le gallic committed
39 40
def getpath(filename, backup=False):
    """Récupère le chemin du fichier ``filename``"""
Vincent Le gallic's avatar
Vincent Le gallic committed
41
    return os.path.join(serverconfig.STORE, '%s.%s' % (filename, 'bak' if backup else 'json'))
Daniel STAN's avatar
init  
Daniel STAN committed
42 43

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

def listroles():
    """Liste des roles existant et de leurs membres"""
Vincent Le gallic's avatar
Vincent Le gallic committed
52
    return serverconfig.ROLES
Daniel STAN's avatar
init  
Daniel STAN committed
53 54

def listkeys():
Vincent Le gallic's avatar
Vincent Le gallic committed
55
    """Liste les usernames et les (mail, fingerprint) correspondants"""
Vincent Le gallic's avatar
Vincent Le gallic committed
56
    return serverconfig.KEYS
Daniel STAN's avatar
init  
Daniel STAN committed
57 58 59

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

82 83
def getfiles():
    """Récupère plusieurs fichiers, lit la liste des filenames demandés sur stdin"""
Daniel STAN's avatar
init  
Daniel STAN committed
84
    stdin = sys.stdin.read()
85 86 87 88 89
    filenames = json.loads(stdin)
    return [getfile(f) for f in filenames]

def _putfile(filename, roles, contents):
    """Écrit ``contents`` avec les roles ``roles`` dans le fichier ``filename``"""
90 91
    gotit, old = getfile(filename)
    if not gotit:
Vincent Le gallic's avatar
Vincent Le gallic committed
92
        old = u"[Création du fichier]"
Daniel STAN's avatar
init  
Daniel STAN committed
93 94
        pass
    else:
95 96 97 98 99 100 101
        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)
102
    
103
    filepath = getpath(filename)
Daniel STAN's avatar
init  
Daniel STAN committed
104
    writefile(filepath, json.dumps({'roles': roles, 'contents': contents}))
105
    return [True, u"Modification effectuée."]
Daniel STAN's avatar
init  
Daniel STAN committed
106

107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
def putfile(filename):
    """Écrit le fichier ``filename`` avec les données reçues sur stdin."""
    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"]
    return _putfile(filename, roles, contents)

def putfiles():
    """Écrit plusieurs fichiers. Lit les filenames sur l'entrée standard avec le reste."""
    stdin = sys.stdin.read()
    parsed_stdin = json.loads(stdin)
    results = []
    for fichier in parsed_stdin:
        try:
            filename = fichier['filename']
            roles = fichier['roles']
            contents = fichier['contents']
        except KeyError:
            results.append([False, u"Entrée invalide"])
        else:
            results.append(_putfile(filename, roles, contents))
    return results


Daniel STAN's avatar
init  
Daniel STAN committed
135 136
def rmfile(filename):
    """Supprime le fichier filename après avoir vérifié les droits sur le fichier"""
137 138 139 140 141 142 143 144 145
    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
146
    else:
147 148
        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
149

150

151
def backup(corps, fname, old):
Vincent Le gallic's avatar
Vincent Le gallic committed
152 153
    """Backupe l'ancienne version du fichier"""
    back = open(getpath(fname, backup=True), 'a')
Daniel STAN's avatar
Daniel STAN committed
154 155
    back.write(json.dumps(old))
    back.write('\n')
156
    back.write((u'* %s: %s\n' % (str(datetime.datetime.now()), corps)).encode("utf-8"))
Daniel STAN's avatar
Daniel STAN committed
157
    back.close()
158

Vincent Le gallic's avatar
Vincent Le gallic committed
159 160
def notification(subject, corps, fname, old):
    """Envoie par mail une notification de changement de fichier"""
161
    conn = smtplib.SMTP('localhost')
Vincent Le gallic's avatar
Vincent Le gallic committed
162 163
    frommail = serverconfig.CRANSP_MAIL
    tomail = serverconfig.DEST_MAIL
164 165
    msg = MIMEMultipart(_charset="utf-8")
    msg['Subject'] = subject
Vincent Le gallic's avatar
Vincent Le gallic committed
166 167 168 169
    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
170
    info = MIMEText(corps + 
Vincent Le gallic's avatar
Vincent Le gallic committed
171 172
        u"\nLa version précédente a été sauvegardée." +
        u"\n\n-- \nCranspasswords.py", _charset="utf-8")
173
    msg.attach(info)
Vincent Le gallic's avatar
Vincent Le gallic committed
174
    conn.sendmail(frommail, tomail, msg.as_string())
175 176
    conn.quit()

177 178
WRITE_COMMANDS = ["putfile", "rmfile"]

Daniel STAN's avatar
init  
Daniel STAN committed
179 180 181 182 183
if __name__ == "__main__":
    argv = sys.argv[1:]
    if len(argv) not in [1, 2]:
        sys.exit(1)
    command = argv[0]
Vincent Le gallic's avatar
Vincent Le gallic committed
184
    if serverconfig.READONLY and command in WRITE_COMMANDS:
185
        raise IOError("Ce serveur est read-only.")
Daniel STAN's avatar
init  
Daniel STAN committed
186 187 188 189 190
    filename = None
    try:
        filename = argv[1]
    except IndexError:
        pass
Vincent Le gallic's avatar
Vincent Le gallic committed
191
    
192
    answer = None
Daniel STAN's avatar
init  
Daniel STAN committed
193
    if command == "listroles":
194
        answer = listroles()
Daniel STAN's avatar
init  
Daniel STAN committed
195
    elif command == "listkeys":
196
        answer = listkeys()
Daniel STAN's avatar
init  
Daniel STAN committed
197
    elif command == "listfiles":
198
        answer = listfiles()
199 200 201 202
    elif command == "getfiles":
        answer = getfiles()
    elif command == "putfiles":
        answer = putfiles()
Daniel STAN's avatar
init  
Daniel STAN committed
203 204
    else:
        if not filename:
205
            print("filename nécessaire pour cette opération", file=sys.stderr)
Daniel STAN's avatar
init  
Daniel STAN committed
206 207
            sys.exit(1)
        if command == "getfile":
208
            answer = getfile(filename)
Daniel STAN's avatar
init  
Daniel STAN committed
209
        elif command == "putfile":
210
            answer = putfile(filename)
Daniel STAN's avatar
init  
Daniel STAN committed
211
        elif command == "rmfile":
212
            answer = rmfile(filename)
Daniel STAN's avatar
init  
Daniel STAN committed
213 214
        else:
            sys.exit(1)
215 216
    if not answer is None:
        print(json.dumps(answer))