server.py 7.14 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
    f.close()

def listroles():
51 52 53 54 55 56 57
    """Liste des roles existant et de leurs membres.
       Renvoie également un rôle particulier ``"whoami"``, contenant l'username de l'utilisateur qui s'est connecté."""
    d = serverconfig.ROLES
    if d.has_key("whoami"):
        raise ValueError('La rôle "whoami" ne devrait pas exister')
    d["whoami"] = MYUID
    return d
Daniel STAN's avatar
init  
Daniel STAN committed
58 59

def listkeys():
Vincent Le gallic's avatar
Vincent Le gallic committed
60
    """Liste les usernames et les (mail, fingerprint) correspondants"""
Vincent Le gallic's avatar
Vincent Le gallic committed
61
    return serverconfig.KEYS
Daniel STAN's avatar
init  
Daniel STAN committed
62 63 64

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
65
    os.chdir(serverconfig.STORE)
Vincent Le gallic's avatar
Vincent Le gallic committed
66
    
Daniel STAN's avatar
init  
Daniel STAN committed
67 68 69 70 71 72 73 74
    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
75
    """Récupère le fichier ``filename``"""
Daniel STAN's avatar
init  
Daniel STAN committed
76 77
    filepath = getpath(filename)
    try:
root's avatar
root committed
78 79
        obj = json.loads(open(filepath).read())
        if not validate(obj['roles']):
80
	        return [False, u"Vous n'avez pas les droits de lecture sur le fichier %s." % filename]
81
        obj["filename"] = filename
82
        return [True, obj]
Daniel STAN's avatar
init  
Daniel STAN committed
83
    except IOError:
84
        return [False, u"Le fichier %s n'existe pas." % filename]
Daniel STAN's avatar
init  
Daniel STAN committed
85 86
     

87 88
def getfiles():
    """Récupère plusieurs fichiers, lit la liste des filenames demandés sur stdin"""
Daniel STAN's avatar
init  
Daniel STAN committed
89
    stdin = sys.stdin.read()
90 91 92 93 94
    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``"""
95 96
    gotit, old = getfile(filename)
    if not gotit:
Vincent Le gallic's avatar
Vincent Le gallic committed
97
        old = u"[Création du fichier]"
Daniel STAN's avatar
init  
Daniel STAN committed
98 99
        pass
    else:
100 101 102 103 104 105 106
        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)
107
    
108
    filepath = getpath(filename)
Daniel STAN's avatar
init  
Daniel STAN committed
109
    writefile(filepath, json.dumps({'roles': roles, 'contents': contents}))
110
    return [True, u"Modification effectuée."]
Daniel STAN's avatar
init  
Daniel STAN committed
111

112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
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
140 141
def rmfile(filename):
    """Supprime le fichier filename après avoir vérifié les droits sur le fichier"""
142 143 144 145 146 147 148 149 150
    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
151
    else:
152 153
        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
154

155

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

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

182 183
WRITE_COMMANDS = ["putfile", "rmfile"]

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