Newer
Older
import subprocess
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
try:
from cpasswords import clientlib
except ImportError:
print("Couldn't import clientlib. Remote sync may not work", file=sys.stderr)
# Même problème que pour le client, il faut bootstraper le nom de la commande
# Pour accéder à la config
conf_path = os.getenv('CRANSPASSWORDS_SERVER_CONFIG_DIR', None)
if not conf_path:
cmd_name = os.path.split(sys.argv[0])[1].replace("-server", "")
conf_path = "/etc/%s/" % (cmd_name,)
sys.path.append(conf_path)
MYUID = pwd.getpwuid(os.getuid())[0]
if MYUID == 'root':
MYUID = os.environ['SUDO_USER']
## Fonctions internes au serveur
def validate(roles, mode='r'):
"""Vérifie que l'appelant appartient bien aux roles précisés
Si mode mode='w', recherche un rôle en écriture
"""
if serverconfig.ROLES.has_key(role) and MYUID in serverconfig.ROLES[role]:
def getpath(filename, backup=False):
"""Récupère le chemin du fichier ``filename``"""
assert(isinstance(filename, unicode))
filename = filename.encode('utf-8')
return os.path.join(serverconfig.STORE, '%s.%s' % (filename, 'bak' if backup else 'json'))
"""Écrit le fichier avec les bons droits UNIX"""
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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
class server_command(object):
"""
Une instance est un décorateur pour la fonction servant de commande
externe du même nom"""
#: nom de la commande
name = None
#: fonction wrappée
decorated = None
#: (static) dictionnaire name => fonction
by_name = {}
#: rajoute un argument en fin de fonction à partir de stdin (si standalone)
stdin_input = False
#: Est-ce que ceci a besoin d'écrire ?
write = False
def __init__(self, name, stdin_input = False, write=False):
"""
* ``name`` nom de l'action telle qu'appelée par le client
* ``stdin_input`` si True, stdin sera lu en mode non-keepalive, et
remplira le dernier argument de la commande.
* ``write`` s'agit-il d'une commande en écriture ?
"""
self.name = name
self.stdin_input = stdin_input
self.write = write
server_command.by_name[name] = self
def __call__(self, fun):
self.decorated = fun
return fun
## Fonction exposées par le serveur
@server_command('keep-alive')
def keepalive():
""" Commande permettant de réaliser un tunnel json (un datagramme par ligne)
Un message entre le client et le serveur consiste en l'échange de dico
Message du client: {'action': "nom_de_l'action",
'args': liste_arguments_passes_a_la_fonction}
Réponse du serveur: {'status': 'ok',
'content': retour_de_la_fonction,
}
"""
for line in iter(sys.stdin.readline, ''):
data = json.loads(line.rstrip())
try:
# Une action du protocole = de l'ascii
action = data['action'].encode('ascii')
content = server_command.by_name[action].decorated(*data['args'])
status = u'ok'
except Exception as e:
status = u'error'
content = repr(e)
out = {
'status': status,
'content': content,
}
print(json.dumps(out, encoding='utf-8'))
sys.stdout.flush()
@server_command('listroles')
Vincent Le gallic
committed
"""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é."""
Vincent Le gallic
committed
d = serverconfig.ROLES
if d.has_key("whoami"):
raise ValueError('La rôle "whoami" ne devrait pas exister')
d["whoami"] = MYUID
return d
@server_command('listkeys')
"""Liste les usernames et les (mail, fingerprint) correspondants"""
@server_command('listfiles')
def listfiles():
"""Liste les fichiers dans l'espace de stockage, et les roles qui peuvent y accéder"""
filenames = glob.glob('*.json')
files = {}
for filename in filenames:
file_dict = json.loads(open(filename).read())
fname = filename[:-5].decode('utf-8')
files[fname] = file_dict["roles"]
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
@server_command('restorefiles')
def restorefiles():
"""Si un fichier a été corrompu, on restore son dernier backup valide"""
os.chdir(serverconfig.STORE)
filenames = glob.glob('*.json')
files = {}
for filename in filenames:
file_dict = json.loads(open(filename).read())
if not ('-----BEGIN PGP MESSAGE-----' in file_dict["contents"]):
fname = filename[:-5].decode('utf-8')
with open(fname+'.bak') as f:
line = f.readline()
backup = ''
while not (line==''):
try:
line_dict = json.loads(line)
if ('-----BEGIN PGP MESSAGE-----' in line_dict["contents"]):
backup = line
except:
pass
line = f.readline()
if not (backup == ''):
files[fname] = 'restored'
with open(fname+'.json','w') as f2:
f2.write(backup)
else:
files[fname] = 'not restored'
return files
@server_command('getfile')
obj = json.loads(open(filepath).read())
if not validate(obj['roles']):
return [False, u"Vous n'avez pas les droits de lecture sur le fichier %s." % filename]
obj["filename"] = filename
return [False, u"Le fichier %s n'existe pas." % filename]
@server_command('getfiles', stdin_input=True)
def getfiles(filenames):
"""Récupère plusieurs fichiers, lit la liste des filenames demandés sur stdin"""
return [getfile(f) for f in filenames]
# TODO ça n'a rien à faire là, à placer plus haut dans le code
def _putfile(filename, roles, contents):
"""Écrit ``contents`` avec les roles ``roles`` dans le fichier ``filename``
"""
gotit, old = getfile(filename)
if not gotit:
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", filename, MYUID)
filepath = getpath(filename)
return [False, u"Erreur: merci de patcher votre cpasswords !"
+ "(contents should be encrypted str)"]
# Or fuck yourself
writefile(filepath, json.dumps({'roles': roles, 'contents': contents}))
data = {'filename': filename, 'roles': roles, 'contents': contents}
for client in _list_to_replicate(data):
client.put_file(data)
return [True, u"Modification effectuée."]
@server_command('putfile', stdin_input=True, write=True)
def putfile(filename, parsed_stdin):
"""Écrit le fichier ``filename`` avec les données reçues sur stdin."""
try:
roles = parsed_stdin['roles']
contents = parsed_stdin['contents']
except KeyError:
return [False, u"Entrée invalide"]
return _putfile(filename, roles, contents)
@server_command('putfiles', stdin_input=True, write=True)
def putfiles(parsed_stdin):
"""Écrit plusieurs fichiers. Lit les filenames sur l'entrée standard avec le
reste."""
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
@server_command('rmfile', write=True)
def rmfile(filename):
"""Supprime le fichier filename après avoir vérifié les droits sur le fichier"""
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", filename, MYUID)
return u"Vous n'avez pas les droits d'écriture sur le fichier %s." % filename
return u"Suppression effectuée"
def backup(corps, fname, old):
back = open(getpath(fname, backup=True), 'a')
back.write(json.dumps(old))
back.write('\n')
back.write((u'* %s: %s\n' % (str(datetime.datetime.now()), corps)).encode("utf-8"))
Vincent Le gallic
committed
def _list_to_replicate(data):
"""Renvoie une liste d'options clients sur lesquels appliquer relancer
la procédure (pour réplication auto)"""
roles = data.get('roles', [])
backups = getattr(serverconfig, 'BACKUP_ROLES', {})
servers = getattr(serverconfig, 'BACKUP_SERVERS', {})
configs = set(name for role in roles for name in backups.get(role, []))
return [ clientlib.Client(servers[name]) for name in configs ]
_notif_todo = []
def notification(action, fname, actor):
"""Enregistre une notification"""
_notif_todo.append((action, fname, actor))
def notification_mail():
"""Envoie par mail une notification de changement de fichier"""
if not _notif_todo:
return
frommail = serverconfig.CRANSP_MAIL
tomail = serverconfig.DEST_MAIL
actions = set( task[1] for task in _notif_todo )
msg = MIMEMultipart(_charset="utf-8")
msg['Subject'] = u"Modification de la base (%s)" % (', '.join(actions))
msg['X-Mailer'] = serverconfig.cmd_name.decode()
msg['From'] = frommail
msg['To'] = tomail
msg.preamble = u"%s report" % (serverconfig.cmd_name.decode(),)
liste = (u" * %s de %s par %s" % task for task in _notif_todo)
info = MIMEText(u"Des modifications ont été faites:\n" +
u"\n".join(liste) +
u"\n\nDes sauvegardes ont été réalisées." +
u"\n\nModification effectuée sur %s." % socket.gethostname() +
mailProcess = subprocess.Popen([serverconfig.sendmail_cmd, "-t"], stdin=subprocess.PIPE)
mailProcess.communicate(msg.as_string())
argv = sys.argv[0:]
command_name = argv[1]
command = server_command.by_name[command_name]
if serverconfig.READONLY and command.write:
raise IOError("Ce serveur est read-only.")
args = argv[2:]
# On veut des unicode partout
args = [ s.decode('utf-8') for s in args ]
if command.stdin_input:
args.append(json.loads(sys.stdin.read()))
answer = command.decorated(*args)
if answer is not None: