scripts/utils/ldapcertfs.py
Valentin Samir ff6a3f8e57 [utils/ldapcertfs] Actualisation régulière des fichier de /etc/ssl/crans
À la lecture des fichiers en fonction de la base ldap
2014-10-25 22:36:46 +02:00

484 lines
18 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)
CACHE = {}
CACHE_TIMEOUT = 60
def set_to_cache(keys, value, now=None, null=False):
global CACHE
if now is None:
now = time.time()
if not isinstance(keys, list):
keys = [keys]
for key in keys:
if key or null:
CACHE[key] = (now, value)
def get_from_cache(key, now=None, expire=CACHE_TIMEOUT):
global CACHE
if now is None:
now = time.time()
if key in CACHE:
(ts, v) = CACHE[key]
if now - ts < expire:
return v
else:
raise ValueError("Expired")
else:
raise ValueError("Not Found")
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.conn = lc_ldap.shortcuts.lc_ldap_readonly()
self._storage = {}
self.passphrase = {}
def search_cache(self, filter, **kwargs):
try:
return get_from_cache(filter)
except ValueError:
objects = self.conn.search(filter, **kwargs)
set_to_cache(filter, objects)
return objects
def make_root(self):
self._storage['/'] = Item(0755 | stat.S_IFDIR, self.uid, self.gid)
if self.ldap_filter == 'self':
machines = self.search_cache(u'mid=*', dn=self.conn.dn, scope=1)
elif self.ldap_filter:
machines = self.search_cache("(&(%s)(mid=*))" % self.ldap_filter, sizelimit=8000)
else:
machines = self.conn.get_local_machines()
for machine in machines:
if not machine.certificats():
continue
if 'aid' in machine.dn and "cransAccount" in machine.proprio()['objectClass']:
self.uid = int(machine.proprio()["uidNumber"][0])
self.gid = int(machine.proprio()["gidNumber"][0])
else:
self.uid = 0
self.gid = 0
mpath = "/%s" % machine['host'][0]
self._storage[mpath]=Item(0755 | stat.S_IFDIR, self.uid, self.gid)
self._add_to_parent_dir(mpath)
def make_machine(self, hostname):
machine = self.search_cache(u"host=%s" % hostname)[0]
mpath = '/%s' % machine['host'][0]
self._storage[mpath]=Item(0755 | stat.S_IFDIR, self.uid, self.gid)
for cert in machine.certificats(refresh=True):
xpath = '%s/xid=%s' % (mpath, cert["xid"][0])
self._storage[xpath]=Item(0755 | stat.S_IFDIR, self.uid, self.gid)
self._add_to_parent_dir(xpath)
def make_cert(self, xid):
cert = self.search_cache(u"xid=%s" % xid)[0]
xpath = '/%s/xid=%s' % (cert.machine()['host'][0], cert["xid"][0])
self._storage[xpath]=Item(0755 | stat.S_IFDIR, self.uid, self.gid)
for file, file_data in self.files.items():
self.make_file(xid, file)
def make_file(self, xid, file):
cert = self.search_cache(u"xid=%s" % xid)[0]
xpath = '/%s/xid=%s' % (cert.machine()['host'][0], cert["xid"][0])
file_data = self.files[file]
data = self._file_data(cert.machine(), cert, file)
if data:
fpath = '%s/%s' % (xpath, file)
self._storage[fpath]=Item(file_data['mode'] | stat.S_IFREG, self.uid, self.gid)
self._storage[fpath].data = data
self._add_to_parent_dir(fpath)
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
def parse_path(self, path):
machine_host = None
xid = None
file = None
paths = path.split('/')[1:]
if paths:
machine_host = paths[0]
paths = paths[1:]
if paths:
if paths[0].startswith("xid="):
xid = paths[0][4:]
paths = paths[1:]
if paths:
file = paths[0]
return (machine_host, xid, file)
def build_path(self, path):
try:
get_from_cache(path)
except ValueError:
if path in self._storage:
del self._storage[path]
if path == "/":
self.make_root()
else:
(machine_host, xid, file) = self.parse_path(path)
if machine_host and not xid and not file:
self.make_machine(machine_host)
elif machine_host and xid and not file:
self.make_cert(xid)
elif machine_host and xid and file:
self.make_file(xid, file)
set_to_cache(path, True)
# --- Metadata -----------------------------------------------------------
def getattr(self, path):
self.build_path(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)