[utils/lc_ldap_fs] Un système fuse jouet pour explorer la base ldap dans un système de fichier

En lecture seule bien entendu.
This commit is contained in:
Valentin Samir 2014-10-30 17:54:01 +01:00
parent 6ce8fe2497
commit 1f3a7d2ea2

456
utils/lc_ldap_fs.py Executable file
View file

@ -0,0 +1,456 @@
#!/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 ldap
import time
import fuse
import errno
import getpass
from OpenSSL import crypto
import lc_ldap.shortcuts
import lc_ldap.attributs
import gestion.secrets_new as secrets
# Specify what Fuse API use: 0.2
fuse.fuse_python_api = (0, 2)
CACHE = {}
CACHE_TIMEOUT = 600
DEBUG = False
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 or expire < 0:
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, *args, **kwargs):
fuse.Fuse.__init__(self, *args, **kwargs)
self.uid = os.getuid()
self.gid = os.getgid()
self.basedn = 'ou=data,dc=crans,dc=org'
self.conn = lc_ldap.shortcuts.lc_ldap_readonly()
self._storage = {}
self.path_to_dn_alias = {}
self.dn_to_path_alias = {}
def _func_cache(self, func, *args, **kwargs):
rec = kwargs.pop('rec', False)
kwargs_l = ["%s=%s" % kv for kv in kwargs.items()]
kwargs_l.sort()
serial = "%s(%s,%s)" % (func.__name__, ", ".join(args), ", ".join(kwargs_l))
try:
return get_from_cache(serial)
except ValueError:
try:
objects = func(*args, **kwargs)
set_to_cache(serial, objects)
return objects
except ldap.SERVER_DOWN:
try:
self.conn = lc_ldap.shortcuts.lc_ldap_readonly()
if not rec:
logger.warning("[ldapcertfs] ldap down, retrying")
kwargs['rec'] = rec
self.func_cache(func, *args, **kwargs)
except Exception as e:
logger.error("[ldapcertfs] uncaught exception %r" % e)
# Si le serveur est down on essaye de fournir un ancienne valeur du cache
try:
return get_from_cache(serial, expire=-1)
except ValueError:
logger.critical("[ldapcertfs] fail to return a valid result, I will probably crash next to this")
return []
def search_cache(self, *args, **kwargs):
return self._func_cache(self.conn.search, *args, **kwargs)
def is_singlevalue(self, attr):
try:
return getattr(lc_ldap.attributs, attr).singlevalue
except AttributeError:
return False
def dn_to_path(self, dn):
if dn == self.basedn:
return '/'
if '/' in dn:
raise ValueError('/ in dn is invalid')
else:
if not dn.endswith(self.basedn):
raise ValueError("dn %s outside of %s" % (dn, self.basedn))
else:
dn = dn[0:-len(self.basedn)-1]
dn = dn.split(',')
dn.reverse()
return "/" + "/".join(dn)
def path_to_dn(self, path):
path = path[1:]
if ',' in path:
raise ValueError(', in path is invalid')
path = path.split('/')
path.reverse()
attr = None
index = None
if path and not '=' in path[0]:
try:
index=int(path[0])
attr=path[1]
path=path[2:]
except ValueError:
attr=path[0]
del path[0]
vdn=",".join(path)
path.reverse()
if vdn:
dn = "%s,%s" % (vdn, self.basedn)
else:
dn = self.basedn
return (dn, attr, index)
def make(self, path):
try:
get_from_cache(path)
except ValueError:
try:
(dn, attr, index) = self.path_to_dn(path)
if attr:
self.make_attr(dn, attr, index)
else:
self.make_dn(dn)
except ldap.INVALID_DN_SYNTAX:
DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('INVALID_DN_SYNTAX: %s\n' % path)
except KeyError:
DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('KeyError: %s\n' % path)
set_to_cache(path, True)
def make_attr(self, dn, attr, index):
DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('dn=%s | attr=%s\n' % (dn, attr))
attr_data = self.search_cache(dn=dn, scope=0, sizelimit=-1)[0].attrs[attr]
path = self.dn_to_path(dn)
apath = os.path.join(path, attr)
if self.is_singlevalue(attr):
self._storage[apath]=Item(0400 | stat.S_IFREG, self.uid, self.gid)
self._storage[apath].data = str(attr_data[0]) + "\n"
self._add_to_parent_dir(apath)
else:
if index is None:
self._storage[apath]=Item(0500 | stat.S_IFDIR, self.uid, self.gid)
self._add_to_parent_dir(apath)
for i in range(0, len(attr_data)):
ipath=os.path.join(apath, str(i))
if not ipath in self._storage:
self._storage[ipath] = Item(0400 | stat.S_IFREG, self.uid, self.gid)
self._storage[ipath].data = str(attr_data[i]) + "\n"
self._add_to_parent_dir(ipath)
else:
ipath = os.path.join(apath, str(index))
self._add_to_parent_dir(ipath)
self._storage[ipath]=Item(0400 | stat.S_IFREG, self.uid, self.gid)
self._storage[ipath].data = str(attr_data[index]) + "\n"
def make_dn(self, dn):
DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('dn=%s\n' % dn)
path = self.dn_to_path(dn)
objects = self.search_cache(dn=dn, scope=1, sizelimit=-1)
try:
current_obj = self.search_cache(dn=dn, scope=0, sizelimit=-1)[0]
spath = os.path.join(os.path.dirname(path), str(current_obj))
self._storage[spath] = Item(0644 | stat.S_IFLNK, self.uid, self.gid)
self._storage[spath].data = os.path.basename(path)
self._add_to_parent_dir(spath)
attrs = current_obj.attrs
except ldap.NO_SUCH_OBJECT:
attrs = {}
DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('path=%s\n' % path)
self._storage[path] = Item(0500 | stat.S_IFDIR, self.uid, self.gid)
self._add_to_parent_dir(path)
for obj in objects:
opath = self.dn_to_path(obj.dn)
if not opath in self._storage:
self._storage[opath]=Item(0555 | stat.S_IFDIR, self.uid, self.gid)
self._add_to_parent_dir(opath)
spath = os.path.join(os.path.dirname(opath), str(obj))
self._storage[spath] = Item(0644 | stat.S_IFLNK, self.uid, self.gid)
self._storage[spath].data = os.path.basename(opath)
self._add_to_parent_dir(spath)
for attr in attrs:
apath = os.path.join(path, attr)
if not apath in self._storage:
if not self.is_singlevalue(attr):
self._storage[apath]=Item(0555 | stat.S_IFDIR, self.uid, self.gid)
else:
self._storage[apath]=Item(0400 | stat.S_IFREG, self.uid, self.gid)
self._add_to_parent_dir(apath)
# --- Metadata -----------------------------------------------------------
def getattr(self, path):
try:
self.make(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
except Exception as e:
DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('%r\n' % e)
return -errno.ENOENT
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)
if parent_path in self._storage and filename:
DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('add %s to parent %s\n' % (filename, parent_path))
self._storage[parent_path].data.add(filename)
else:
DEBUG and open('/tmp/lc_ldap_fs.log', 'a+').write('add %s to parent %s but parent is unknown\n' % (filename, parent_path))
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):
# 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(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)