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

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

Daniel Stan's avatar
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
Daniel Stan committed
17

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
Daniel Stan committed
24 25 26 27
MYUID = pwd.getpwuid(os.getuid())[0]
if MYUID == 'root':
    MYUID = os.environ['SUDO_USER']

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

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

def writefile(filename, contents):
44
    """Écrit le fichier avec les bons droits UNIX"""
Daniel Stan's avatar
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
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
Daniel Stan committed
58 59

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

def listfiles():
    """Liste les fichiers dans l'espace de stockage, et les roles qui peuvent y accéder"""
65
    os.chdir(serverconfig.STORE)
66
    
Daniel Stan's avatar
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):
75
    """Récupère le fichier ``filename``"""
Daniel Stan's avatar
Daniel Stan committed
76 77
    filepath = getpath(filename)
    try:
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
Daniel Stan committed
83
    except IOError:
84
        return [False, u"Le fichier %s n'existe pas." % filename]
Daniel Stan's avatar
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
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
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
Daniel Stan committed
109
    writefile(filepath, json.dumps({'roles': roles, 'contents': contents}))
110
    return [True, u"Modification effectuée."]
Daniel Stan's avatar
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
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
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
Daniel Stan committed
154

155

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

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

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

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