456 lines
16 KiB
Python
Executable file
456 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 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)
|
|
|