From 1f3a7d2ea27dfe0da7969b3d30d82266c2c73503 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Thu, 30 Oct 2014 17:54:01 +0100 Subject: [PATCH] =?UTF-8?q?[utils/lc=5Fldap=5Ffs]=20Un=20syst=C3=A8me=20fu?= =?UTF-8?q?se=20jouet=20pour=20explorer=20la=20base=20ldap=20dans=20un=20s?= =?UTF-8?q?yst=C3=A8me=20de=20fichier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit En lecture seule bien entendu. --- utils/lc_ldap_fs.py | 456 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100755 utils/lc_ldap_fs.py diff --git a/utils/lc_ldap_fs.py b/utils/lc_ldap_fs.py new file mode 100755 index 00000000..f6add2ae --- /dev/null +++ b/utils/lc_ldap_fs.py @@ -0,0 +1,456 @@ +#!/bin/bash /usr/scripts/python.sh +#-*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright (c) 2010, Matteo Bertozzi +# Copyright (c) 2014, Valentin Samir +# 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) +