#!/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)