419 lines
16 KiB
Python
Executable file
419 lines
16 KiB
Python
Executable file
#!/bin/bash /usr/scripts/python.sh
|
|
#-*- coding: utf-8 -*-
|
|
# ----------------------------------------------------------------------------
|
|
# Copyright (c) 2010, Matteo Bertozzi <theo.bertozzi@gmail.com>
|
|
# Copyright (c) 2014, Valentin Samir <valentin.samir@crans.org>
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
# * Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
# * Neither the name of the Matteo Bertozzi nor the
|
|
# names of its contributors may be used to endorse or promote products
|
|
# derived from this software without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY Matteo Bertozzi ``AS IS'' AND ANY
|
|
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
# DISCLAIMED. IN NO EVENT SHALL Matteo Bertozzi BE LIABLE FOR ANY
|
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
# ----------------------------------------------------------------------------
|
|
# How to mount the File-System:
|
|
# python ldapcertfs mnt [ldap_filter]
|
|
#
|
|
# How to umount the File-System?
|
|
# fusermount -u mnt
|
|
|
|
import os
|
|
import sys
|
|
import ssl
|
|
import stat
|
|
import time
|
|
import fuse
|
|
import errno
|
|
import getpass
|
|
|
|
from OpenSSL import crypto
|
|
|
|
import lc_ldap.shortcuts
|
|
import gestion.secrets_new as secrets
|
|
|
|
# Specify what Fuse API use: 0.2
|
|
fuse.fuse_python_api = (0, 2)
|
|
|
|
import logging
|
|
LOG_FILENAME = 'ldapcertfs.log'
|
|
logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG)
|
|
|
|
class Pass:
|
|
pass
|
|
|
|
class Item(object):
|
|
"""
|
|
An Item is an Object on Disk, it can be a Directory, File, Symlink, ...
|
|
"""
|
|
def __init__(self, mode, uid, gid):
|
|
# ----------------------------------- Metadata --
|
|
self.atime = time.time() # time of last acces
|
|
self.mtime = self.atime # time of last modification
|
|
self.ctime = self.atime # time of last status change
|
|
|
|
self.dev = 0 # device ID (if special file)
|
|
self.mode = mode # protection and file-type
|
|
self.uid = uid # user ID of owner
|
|
self.gid = gid # group ID of owner
|
|
|
|
# ------------------------ Extended Attributes --
|
|
self.xattr = {}
|
|
|
|
# --------------------------------------- Data --
|
|
if stat.S_ISDIR(mode):
|
|
self.data = set()
|
|
else:
|
|
self.data = ''
|
|
|
|
def read(self, offset, length):
|
|
return self.data[offset:offset+length]
|
|
|
|
def write(self, offset, data):
|
|
length = len(data)
|
|
self.data = self.data[:offset] + data + self.data[offset+length:]
|
|
return length
|
|
|
|
def truncate(self, length):
|
|
if len(self.data) > length:
|
|
self.data = self.data[:length]
|
|
else:
|
|
self.data += '\x00'# (length - len(self.data))
|
|
|
|
def zstat(stat):
|
|
stat.st_mode = 0
|
|
stat.st_ino = 0
|
|
stat.st_dev = 0
|
|
stat.st_nlink = 2
|
|
stat.st_uid = 0
|
|
stat.st_gid = 0
|
|
stat.st_size = 0
|
|
stat.st_atime = 0
|
|
stat.st_mtime = 0
|
|
stat.st_ctime = 0
|
|
return stat
|
|
|
|
class LdapCertFS(fuse.Fuse):
|
|
def __init__(self, ldap_filter, nopkey=False, decrypt=False, *args, **kwargs):
|
|
fuse.Fuse.__init__(self, *args, **kwargs)
|
|
|
|
self.uid = os.getuid()
|
|
self.gid = os.getgid()
|
|
|
|
self.nopkey=nopkey
|
|
self.decrypt = decrypt
|
|
self.ldap_filter = ldap_filter
|
|
|
|
# dictionnnaire CN => certificat pour construire la chaine de certificat
|
|
# Il n'est utile ici que de renseigner des CN de CA intermédiaires
|
|
self.chain = {
|
|
'CAcert Class 3 Root' : open('/etc/ssl/certs/cacert.org.pem').read(),
|
|
}
|
|
# Les fichers certificats que l'on veux créer. fil est une liste représentant
|
|
# la concaténation des attributs ldap (bien formaté). chain est un joker pour
|
|
# construire la chaine de certificat
|
|
self.files = {
|
|
'crt.pem' : {'file':['certificat'], 'mode':0444},
|
|
'key.pem' : {'file':['privatekey'], 'mode':0400},
|
|
'csr.pem' : {'file':['csr'], 'mode':0444},
|
|
'chain.pem' : {'file':['chain'], 'mode':0444},
|
|
'key_cert_chain.pem' : {'file':['privatekey', 'certificat', 'chain'], 'mode':0400},
|
|
'cert_chain.pem' : {'file':['certificat', 'chain'], 'mode':0400},
|
|
}
|
|
self.build_tree()
|
|
|
|
def build_tree(self):
|
|
"""Construit l'arborescence du système de fichier"""
|
|
self._storage = {'/': Item(0755 | stat.S_IFDIR, self.uid, self.gid)}
|
|
self.passphrase = {}
|
|
conn = lc_ldap.shortcuts.lc_ldap_readonly()
|
|
if self.ldap_filter == 'self':
|
|
machines = conn.search(u'mid=*', dn=conn.dn, scope=1)
|
|
elif self.ldap_filter:
|
|
machines = conn.search("(&(%s)(mid=*))" % self.ldap_filter, sizelimit=8000)
|
|
else:
|
|
machines = conn.get_local_machines()
|
|
|
|
for machine in machines:
|
|
if not machine.certificats():
|
|
continue
|
|
if 'aid' in machine.dn and "cransAccount" in machine.proprio()['objectClass']:
|
|
uid = int(machine.proprio()["uidNumber"][0])
|
|
gid = int(machine.proprio()["gidNumber"][0])
|
|
else:
|
|
uid = 0
|
|
gid = 0
|
|
mpath = "/%s" % machine['host'][0]
|
|
self._storage[mpath]=Item(0755 | stat.S_IFDIR, uid, gid)
|
|
self._add_to_parent_dir(mpath)
|
|
for cert in machine.certificats():
|
|
path = '%s/xid=%s' % (mpath, cert["xid"][0])
|
|
self._storage[path]=Item(0755 | stat.S_IFDIR, uid, gid)
|
|
self._add_to_parent_dir(path)
|
|
for file, file_data in self.files.items():
|
|
data = self._file_data(machine, cert, file)
|
|
if data:
|
|
fpath = '%s/%s' % (path, file)
|
|
self._storage[fpath]=Item(file_data['mode'] | stat.S_IFREG, uid, gid)
|
|
self._storage[fpath].data = data
|
|
self._add_to_parent_dir(fpath)
|
|
|
|
for machine in machines:
|
|
if not machine.certificats():
|
|
continue
|
|
mpath = "/%s" % machine['host'][0]
|
|
for cert in machine.certificats():
|
|
path = 'xid=%s' % (cert["xid"][0])
|
|
if cert['info']:
|
|
item = Item(0644 | stat.S_IFLNK, self.uid, self.gid)
|
|
item.data = path
|
|
newpath = '%s/%s' % (mpath, cert['info'][0])
|
|
indice = 1
|
|
while newpath in self._storage:
|
|
newpath = '/%s (%s)' % (cert['info'][0], indice)
|
|
indice+=1
|
|
self._storage[newpath] = item
|
|
self._add_to_parent_dir(newpath)
|
|
|
|
|
|
def _file_data(self, machine, cert, file):
|
|
"""Construit le contenue du fichier file utilisant le certificat cert de machine"""
|
|
data = ""
|
|
for dtype in self.files[file]['file']:
|
|
if dtype == "chain":
|
|
if cert['issuerCN'][0] in self.chain:
|
|
data+=self.chain[str(cert['issuerCN'][0])]
|
|
else:
|
|
return None
|
|
elif dtype == "certificat":
|
|
data+=ssl.DER_cert_to_PEM_cert(str(cert['certificat'][0]))
|
|
elif dtype == "privatekey":
|
|
if "privateKey" in cert['objectClass'] and cert['privatekey'] and not self.nopkey:
|
|
if self.decrypt:
|
|
while True:
|
|
if not cert['xid'][0] in self.passphrase:
|
|
if cert['encrypted'][0]:
|
|
if "machineCrans" in machine["objectClass"]:
|
|
passphrase=secrets.get('privatekey_passphrase')
|
|
else:
|
|
print "Passphrase de la clef %s de %s" % (cert['info'][0] if cert['info'] else ('xid=%s' % cert["xid"][0]), machine['host'][0])
|
|
try:
|
|
passphrase = getpass.getpass()
|
|
except KeyboardInterrupt:
|
|
print "On passe la clef"
|
|
self.passphrase[cert['xid'][0]]=Pass()
|
|
return None
|
|
else:
|
|
passphrase=None
|
|
self.passphrase[cert['xid'][0]]=passphrase
|
|
elif isinstance(self.passphrase[cert['xid'][0]], Pass):
|
|
return None
|
|
try:
|
|
if self.passphrase[cert['xid'][0]]:
|
|
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, str(cert['privatekey'][0]), self.passphrase[cert['xid'][0]])
|
|
else:
|
|
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, str(cert['privatekey'][0]), "")
|
|
break
|
|
except crypto.Error:
|
|
print "mauvais mot de pass"
|
|
del(self.passphrase[cert['xid'][0]])
|
|
data+=str(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
|
else:
|
|
data+=str(cert['privatekey'][0])
|
|
else:
|
|
return None
|
|
elif cert[dtype]:
|
|
data+=str(cert[dtype][0])
|
|
else:
|
|
return None
|
|
return data
|
|
|
|
# --- Metadata -----------------------------------------------------------
|
|
def getattr(self, path):
|
|
if not path in self._storage:
|
|
return -errno.ENOENT
|
|
|
|
# Lookup Item and fill the stat struct
|
|
item = self._storage[path]
|
|
st = zstat(fuse.Stat())
|
|
st.st_mode = item.mode
|
|
st.st_uid = item.uid
|
|
st.st_gid = item.gid
|
|
st.st_dev = item.dev
|
|
st.st_atime = item.atime
|
|
st.st_mtime = item.mtime
|
|
st.st_ctime = item.ctime
|
|
st.st_size = len(item.data)
|
|
return st
|
|
|
|
def chmod(self, path, mode):
|
|
return -errno.EPERM
|
|
|
|
def chown(self, path, uid, gid):
|
|
return -errno.EPERM
|
|
|
|
def utime(self, path, times):
|
|
item = self._storage[path]
|
|
item.ctime = item.mtime = times[0]
|
|
|
|
# --- Namespace ----------------------------------------------------------
|
|
def unlink(self, path):
|
|
return -errno.EPERM
|
|
|
|
def rename(self, oldpath, newpath):
|
|
return -errno.EPERM
|
|
|
|
# --- Links --------------------------------------------------------------
|
|
def symlink(self, path, newpath):
|
|
return -errno.EPERM
|
|
|
|
def readlink(self, path):
|
|
return self._storage[path].data
|
|
|
|
# --- Extra Attributes ---------------------------------------------------
|
|
def setxattr(self, path, name, value, flags):
|
|
return -errno.EPERM
|
|
|
|
def getxattr(self, path, name, size):
|
|
value = self._storage[path].xattr.get(name, '')
|
|
if size == 0: # We are asked for size of the value
|
|
return len(value)
|
|
return value
|
|
|
|
def listxattr(self, path, size):
|
|
attrs = self._storage[path].xattr.keys()
|
|
if size == 0:
|
|
return len(attrs) + len(''.join(attrs))
|
|
return attrs
|
|
|
|
def removexattr(self, path, name):
|
|
return -errno.EPERM
|
|
|
|
# --- Files --------------------------------------------------------------
|
|
def mknod(self, path, mode, dev):
|
|
return -errno.EPERM
|
|
|
|
def create(self, path, flags, mode):
|
|
return -errno.EPERM
|
|
|
|
def truncate(self, path, len):
|
|
return -errno.EPERM
|
|
|
|
def read(self, path, size, offset):
|
|
return self._storage[path].read(offset, size)
|
|
|
|
def write(self, path, buf, offset):
|
|
return -errno.EPERM
|
|
|
|
# --- Directories --------------------------------------------------------
|
|
def mkdir(self, path, mode):
|
|
return -errno.EPERM
|
|
|
|
def rmdir(self, path):
|
|
return -errno.EPERM
|
|
|
|
def readdir(self, path, offset):
|
|
dir_items = self._storage[path].data
|
|
for item in dir_items:
|
|
yield fuse.Direntry(item)
|
|
|
|
def _add_to_parent_dir(self, path):
|
|
parent_path = os.path.dirname(path)
|
|
filename = os.path.basename(path)
|
|
self._storage[parent_path].data.add(filename)
|
|
|
|
def _remove_from_parent_dir(self, path):
|
|
parent_path = os.path.dirname(path)
|
|
filename = os.path.basename(path)
|
|
self._storage[parent_path].data.remove(filename)
|
|
|
|
def main(usage):
|
|
# valeurs par defaut des options
|
|
decrypt=False
|
|
nopkey=False
|
|
ldap_filter = 'self'
|
|
|
|
# Récupération de l'option decrypt
|
|
if '--decrypt' in sys.argv[2:]:
|
|
decrypt=True
|
|
del(sys.argv[sys.argv.index('--decrypt')])
|
|
|
|
# Récupération de l'option nopkey
|
|
if '--nopkey' in sys.argv[2:]:
|
|
nopkey=True
|
|
del(sys.argv[sys.argv.index('--nopkey')])
|
|
|
|
# Récupération de l'option ldap-filter
|
|
try:
|
|
ldap_filter = unicode(sys.argv[sys.argv.index('--ldap-filter') + 1])
|
|
if os.getuid() != 0:
|
|
raise EnvironmentError("Il faut être root pour choisir le filtre ldap")
|
|
del(sys.argv[sys.argv.index('--ldap-filter') + 1])
|
|
except (IndexError, ValueError):
|
|
if os.getuid() == 0:
|
|
ldap_filter = None
|
|
finally:
|
|
try: del(sys.argv[sys.argv.index('--ldap-filter')])
|
|
except (IndexError, ValueError): pass
|
|
|
|
# Vérification que le point de montage est bien un dossier
|
|
end_option = False
|
|
for item in sys.argv[1:]:
|
|
if end_option or not item.startswith('-'):
|
|
if not os.path.isdir(item):
|
|
raise EnvironmentError("%s is not a dir" % item)
|
|
break
|
|
if item == '--':
|
|
end_option=True
|
|
|
|
# Instanciation du FS
|
|
server = LdapCertFS(ldap_filter, nopkey=nopkey, decrypt=decrypt, version="%prog " + fuse.__version__,
|
|
usage=usage,
|
|
dash_s_do='setsingle')
|
|
|
|
server.parse(errex=1)
|
|
server.main()
|
|
|
|
if __name__ == '__main__':
|
|
usage="""
|
|
LdapCertFS - Ldap Certificate File System
|
|
Les obtions spécifiques sont :
|
|
* --decrypt : pour déchiffrer les clefs privées (un prompt demande le
|
|
mot de passe si nécessaire.
|
|
* --nopkey : exclure les clefs privées lors de la construction du FS.
|
|
* --ldap-filter filtre : selectionner les machines à utiliser pour
|
|
construire le FS avec un filtre ldap. Nécéssite les droits root.
|
|
|
|
Si --ldap-filter n'est pas spécifier :
|
|
* Si le programme est appelé par root, on utilises les machines
|
|
correspondants aux ips des interfaces de la machine physique.
|
|
* Sinon, on utilise les machines de l'utilisateur dans la base
|
|
de donnée.
|
|
|
|
""" + fuse.Fuse.fusage
|
|
# On force à fornir au moint un paramètre (il faut au moins un point de montage)
|
|
if len(sys.argv)<2:
|
|
sys.stderr.write("%s\n" % usage.replace('%prog', sys.argv[0]))
|
|
sys.exit(1)
|
|
# On appel main et on affiche les exceptions EnvironmentError
|
|
try:
|
|
main(usage)
|
|
except (EnvironmentError, fuse.FuseError) as e:
|
|
sys.stderr.write("Error: %s\n" % e)
|
|
sys.exit(1)
|
|
|