Switch to side-by-side view

--- a
+++ b/fuse/accessfs.py
@@ -0,0 +1,492 @@
+#!/usr/bin/env python
+import os
+import json
+import urllib
+import urllib2
+import sys
+import pwd
+import errno
+import fcntl
+import logging
+import time
+
+from threading import Lock
+from collections import deque
+
+import fuse
+
+log = logging.getLogger(__name__)
+
+logging.basicConfig()
+
+fuse.fuse_python_api = (0, 2)
+fuse.feature_assert('stateful_files', 'has_init')
+
+class check_access(object):
+
+    def __init__(self, *args, **kwargs):
+        self._args = args
+        self._kwargs = kwargs
+
+    def __call__(self, func):
+        def wrapper(inst, *args, **kwargs):
+            new_args = list(args)
+            new_kwargs = dict(kwargs)
+            for i, (mode, path) in enumerate(zip(self._args, args)):
+                new_args[i] = self.check(inst, path, mode)
+            for name, mode in self._kwargs.iteritems():
+                new_kwargs[name] = self.check(inst, kwargs.get(name), mode)
+            return func(inst, *new_args, **new_kwargs)
+        return wrapper
+
+    def check(self, inst, path, mode):
+        if mode is None: return
+        rc = inst.access(path, mode)
+        if rc:
+            raise OSError(errno.EPERM, path,'Permission denied')
+
+class check_and_translate(check_access):
+
+    def check(self, inst, path, mode):
+        super(check_and_translate, self).check(inst, path, mode)
+        return inst._to_global(path)
+
+class LoopbackFS(fuse.Fuse):
+    '''Loopback filesystem with hooks for permission management'''
+
+    def __init__(self, root, *args, **kwargs):
+        super(LoopbackFS, self).__init__(*args, **kwargs)
+        self._root = root
+
+    @check_and_translate(os.R_OK)
+    def getattr(self, path): return os.lstat(path)
+
+    @check_and_translate(os.R_OK)
+    def readlink(self, path): return os.readlink(path)
+
+    @check_and_translate(os.W_OK)
+    def mknod(self, path, mode, dev): os.mknod(path, mode, dev)
+
+    @check_and_translate(os.W_OK)
+    def mkdir(self, path, mode): os.mkdir(path, mode)
+
+    @check_and_translate(os.W_OK)
+    def unlink(self, path): os.unlink(path)
+
+    @check_and_translate(None, os.W_OK)
+    def symlink(self, target, name): os.symlink(target, name)
+
+    @check_and_translate(os.R_OK|os.W_OK, os.R_OK|os.W_OK)
+    def rename(self, old, new): os.rename(old, new)
+
+    @check_and_translate(os.R_OK, os.W_OK)
+    def link(self, src, dst): os.link(src, dst)
+
+    @check_and_translate(None)
+    def access(self, path, mode): return os.access(path, mode)
+
+    def fsinit(self): pass
+
+    def _to_global(self, path):
+        return os.path.join(self._root, path)
+
+    # @check_access(os.R_OK)
+    # def readdir(self, path, offset):
+    #     for e in os.listdir(self._to_global(path)):
+    #         yield fuse.Direntry(e)
+
+    # def rmdir(self, path):
+    #     self._assert_access(path, os.W_OK)
+    #     os.rmdir(self._to_global(path))
+
+    # def chmod(self, path, mode):
+    #     self._assert_access(path, os.W_OK)
+    #     os.chmod(self._to_global(path), mode)
+
+    # def chown(self, path, user, group):
+    #     self._assert_access(path, os.W_OK)
+    #     os.chown(self._to_global(path), user, group)
+
+    # def truncate(self, path, len):
+    #     self._assert_access(path, os.W_OK)
+    #     f = open(self._to_global(path), "a")
+    #     f.truncate(len)
+    #     f.close()
+
+    # def utime(self, path, times):
+    #     os.utime(self._to_global(path), times)
+
+    # def statfs(self):
+    #     """
+    #     Should return an object with statvfs attributes (f_bsize, f_frsize...).
+    #     Eg., the return value of os.statvfs() is such a thing (since py 2.2).
+    #     If you are not reusing an existing statvfs object, start with
+    #     fuse.StatVFS(), and define the attributes.
+
+    #     To provide usable information (ie., you want sensible df(1)
+    #     output, you are suggested to specify the following attributes:
+
+    #         - f_bsize - preferred size of file blocks, in bytes
+    #         - f_frsize - fundamental size of file blcoks, in bytes
+    #             [if you have no idea, use the same as blocksize]
+    #         - f_blocks - total number of blocks in the filesystem
+    #         - f_bfree - number of free blocks
+    #         - f_files - total number of file inodes
+    #         - f_ffree - nunber of free file inodes
+    #     """
+
+    #     return os.statvfs(".")
+
+    # def make_file_class(self):
+    #     class FSAccessFile(AccessFile):
+    #         filesystem=self
+    #     return FSAccessFile
+
+class OverlayFS(fuse.Fuse):
+    '''Filesystem that overlays a read-only GridFS on an existing filesystem'''
+
+    def __init__(self, root, gfs, *args, **kwargs):
+        super(OverlayFS, self).__init__(*args, **kwargs)
+        self._root = root
+        self._gfs = gfs
+
+
+def flag2mode(flags):
+    md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
+    m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]
+    if flags | os.O_APPEND:
+        m = m.replace('w', 'a', 1)
+    return m
+
+class AccessFS(fuse.Fuse):
+
+    def __init__(self, *args, **kw):
+        super(AccessFS, self).__init__(*args, **kw)
+        self.root = '/'
+        self.auth_method = 'unix'
+        self.permission_host = 'http://localhost:8080'
+        self.permission_cache_timeout = 30
+        self.permission_cache_size = 1024
+        self.file_class = self.make_file_class()
+        self.perm_cache = None
+
+    def getattr(self, path):
+        return os.lstat("." + path)
+
+    def readlink(self, path):
+        self._assert_access(path, os.R_OK)
+        return os.readlink("." + path)
+
+    def readdir(self, path, offset):
+        print 'Readdir!'
+        for e in os.listdir("." + path):
+            yield fuse.Direntry(e)
+
+    def unlink(self, path):
+        self._assert_access(path, os.W_OK)
+        os.unlink("." + path)
+
+    def rmdir(self, path):
+        self._assert_access(path, os.W_OK)
+        os.rmdir("." + path)
+
+    def symlink(self, path, path1):
+        self._assert_access(path, os.W_OK)
+        os.symlink(path, "." + path1)
+
+    def rename(self, path, path1):
+        self._assert_access(path, os.R_OK | os.W_OK)
+        self._assert_access(path1, os.R_OK | os.W_OK)
+        os.rename("." + path, "." + path1)
+
+    def link(self, path, path1):
+        self._assert_access(path, os.R_OK)
+        self._assert_access(path1, os.W_OK)
+        os.link("." + path, "." + path1)
+
+    def chmod(self, path, mode):
+        self._assert_access(path, os.W_OK)
+        os.chmod("." + path, mode)
+
+    def chown(self, path, user, group):
+        self._assert_access(path, os.W_OK)
+        os.chown("." + path, user, group)
+
+    def truncate(self, path, len):
+        self._assert_access(path, os.W_OK)
+        f = open("." + path, "a")
+        f.truncate(len)
+        f.close()
+
+    def mknod(self, path, mode, dev):
+        self._assert_access(path, os.W_OK)
+        os.mknod("." + path, mode, dev)
+
+    def mkdir(self, path, mode):
+        self._assert_access(path, os.W_OK)
+        os.mkdir("." + path, mode)
+
+    def utime(self, path, times):
+        os.utime("." + path, times)
+
+    def access(self, path, mode):
+        if mode & (os.R_OK|os.W_OK) == 0: return
+        ctx = fuse.FuseGetContext()
+        entry = self.perm_cache.get(ctx['uid'], path)
+        if (mode & entry) != mode:
+            return -errno.EACCES
+
+    def _assert_access(self, path, mode):
+        rc = self.access(path, mode)
+        if rc:
+            raise OSError(errno.EPERM, path,'Permission denied')
+
+    def statfs(self):
+        """
+        Should return an object with statvfs attributes (f_bsize, f_frsize...).
+        Eg., the return value of os.statvfs() is such a thing (since py 2.2).
+        If you are not reusing an existing statvfs object, start with
+        fuse.StatVFS(), and define the attributes.
+
+        To provide usable information (ie., you want sensible df(1)
+        output, you are suggested to specify the following attributes:
+
+            - f_bsize - preferred size of file blocks, in bytes
+            - f_frsize - fundamental size of file blcoks, in bytes
+                [if you have no idea, use the same as blocksize]
+            - f_blocks - total number of blocks in the filesystem
+            - f_bfree - number of free blocks
+            - f_files - total number of file inodes
+            - f_ffree - nunber of free file inodes
+        """
+
+        return os.statvfs(".")
+
+    def fsinit(self):
+        uid_cache = UnixUsernameCache()
+        self.perm_cache = PermissionCache(
+            uid_cache,
+            self.permission_host,
+            self.permission_cache_timeout,
+            self.permission_cache_size)
+        os.chdir(self.root)
+
+    def make_file_class(self):
+        class FSAccessFile(AccessFile):
+            filesystem=self
+        return FSAccessFile
+
+class AccessFile(fuse.FuseFileInfo):
+    direct_io=False
+    keep_cache = False
+    needs_write = (
+        os.O_WRONLY
+        | os.O_RDWR
+        | os.O_APPEND
+        | os.O_CREAT
+        | os.O_TRUNC )
+
+    def __init__(self, path, flags, *mode):
+        access_mode = os.R_OK
+        if flags & self.needs_write:
+            access_mode |= os.W_OK
+        self.filesystem._assert_access(path, access_mode)
+        self.file = os.fdopen(os.open("." + path, flags, *mode),
+                              flag2mode(flags))
+        self.fd = self.file.fileno()
+
+    def read(self, length, offset):
+        self.file.seek(offset)
+        return self.file.read(length)
+
+    def write(self, buf, offset):
+        self.file.seek(offset)
+        self.file.write(buf)
+        return len(buf)
+
+    def release(self, flags):
+        self.file.close()
+
+    def _fflush(self):
+        if 'w' in self.file.mode or 'a' in self.file.mode:
+            self.file.flush()
+
+    def fsync(self, isfsyncfile):
+        self._fflush()
+        if isfsyncfile and hasattr(os, 'fdatasync'):
+            os.fdatasync(self.fd)
+        else:
+            os.fsync(self.fd)
+
+    def flush(self):
+        self._fflush()
+        # cf. xmp_flush() in fusexmp_fh.c
+        os.close(os.dup(self.fd))
+
+    def fgetattr(self):
+        return os.fstat(self.fd)
+
+    def ftruncate(self, len):
+        self.file.truncate(len)
+
+    def lock(self, cmd, owner, **kw):
+        # The code here is much rather just a demonstration of the locking
+        # API than something which actually was seen to be useful.
+
+        # Advisory file locking is pretty messy in Unix, and the Python
+        # interface to this doesn't make it better.
+        # We can't do fcntl(2)/F_GETLK from Python in a platfrom independent
+        # way. The following implementation *might* work under Linux.
+        #
+        # if cmd == fcntl.F_GETLK:
+        #     import struct
+        #
+        #     lockdata = struct.pack('hhQQi', kw['l_type'], os.SEEK_SET,
+        #                            kw['l_start'], kw['l_len'], kw['l_pid'])
+        #     ld2 = fcntl.fcntl(self.fd, fcntl.F_GETLK, lockdata)
+        #     flockfields = ('l_type', 'l_whence', 'l_start', 'l_len', 'l_pid')
+        #     uld2 = struct.unpack('hhQQi', ld2)
+        #     res = {}
+        #     for i in xrange(len(uld2)):
+        #          res[flockfields[i]] = uld2[i]
+        #
+        #     return fuse.Flock(**res)
+
+        # Convert fcntl-ish lock parameters to Python's weird
+        # lockf(3)/flock(2) medley locking API...
+        op = { fcntl.F_UNLCK : fcntl.LOCK_UN,
+               fcntl.F_RDLCK : fcntl.LOCK_SH,
+               fcntl.F_WRLCK : fcntl.LOCK_EX }[kw['l_type']]
+        if cmd == fcntl.F_GETLK:
+            return -errno.EOPNOTSUPP
+        elif cmd == fcntl.F_SETLK:
+            if op != fcntl.LOCK_UN:
+                op |= fcntl.LOCK_NB
+        elif cmd == fcntl.F_SETLKW:
+            pass
+        else:
+            return -errno.EINVAL
+
+        fcntl.lockf(self.fd, op, kw['l_start'], kw['l_len'])
+
+class PermissionCache(object):
+
+    def __init__(self, uid_cache, host, timeout=30, size=1024):
+        self._host = host
+        self._timeout = timeout
+        self._size = size
+        self._data = {}
+        self._entries = deque()
+        self._lock = Lock()
+        self._uid_cache = uid_cache
+
+    def get(self, uid, path):
+        try:
+            entry, timestamp = self._data[uid, path]
+            elapsed = time.time() - timestamp
+            if elapsed > self._timeout:
+                print 'Timeout!', elapsed
+                uname = self._uid_cache.get(uid)
+                entry = self._refresh_result(uid, path, self._api_lookup(uname, path))
+                return entry
+            return entry
+        except KeyError:
+            pass
+        uname = self._uid_cache.get(uid)
+        try:
+            entry = self._api_lookup(uname, path)
+        except:
+            entry = 0
+            log.exception('Error checking access for %s', path)
+        self._save_result(uid, path, entry)
+        return entry
+
+    def _api_lookup(self, uname, path):
+        if path.count('/') < 3:
+            return os.R_OK
+        path = self._mangle(path)
+        url = (
+            self._host
+            + '/auth/repo_permissions?'
+            + urllib.urlencode(dict(
+                    repo_path=path,
+                    username=uname)))
+        print 'Checking access for %s at %s (%s)' % (uname, url, path)
+        fp = urllib2.urlopen(url)
+        result = json.load(fp)
+        print result
+        entry = 0
+        if result['allow_read']: entry |= os.R_OK
+        if result['allow_write']: entry |= os.W_OK
+        return entry
+
+    def _refresh_result(self, uid, path, value):
+        with self._lock:
+            if (uid,path) in self._data:
+                self._data[uid, path] = (value, time.time())
+            else:
+                if len(self._data) >= self._size:
+                    k = self._entries.popleft()
+                    del self._data[k]
+                self._data[uid, path] = (value, time.time())
+                self._entries.append((uid, path))
+        return value
+
+    def _save_result(self, uid, path, value):
+        with self._lock:
+            if len(self._data) >= self._size:
+                k = self._entries.popleft()
+                del self._data[k]
+            self._data[uid, path] = (value, time.time())
+            self._entries.append((uid, path))
+
+    def _mangle(self, path):
+        '''Convert paths from the form /SCM/neighborhood/project/a/b/c to
+        /SCM/project.neighborhood/a/b/c
+        '''
+        parts = [ p for p in path.split(os.path.sep) if p ]
+        nbhd, proj, rest = parts[0], parts[1], parts[2:]
+        parts = ['/SCM/%s.%s' % (proj, nbhd) ] + rest
+        return '/'.join(parts)
+
+class UnixUsernameCache(object):
+
+    def __init__(self):
+        self._cache = {}
+
+    def get(self, uid):
+        try:
+            return self._cache[uid]
+        except KeyError:
+            pass
+        uname = pwd.getpwuid(uid).pw_name
+        self._cache[uid] = uname
+        return uname
+
+def main():
+
+    usage = """
+Userspace nullfs-alike: mirror the filesystem tree from some point on.
+
+""" + fuse.Fuse.fusage
+
+    server = AccessFS(version="%prog " + fuse.__version__,
+                 usage=usage,
+                 dash_s_do='setsingle')
+
+    server.parser.add_option(mountopt="root", metavar="PATH", default='/',
+                             help="mirror filesystem from under PATH [default: %default]")
+    server.parse(values=server, errex=1)
+
+    try:
+        if server.fuse_args.mount_expected():
+            os.chdir(server.root)
+    except OSError:
+        print >> sys.stderr, "can't enter root of underlying filesystem"
+        sys.exit(1)
+
+    server.main()
+
+
+if __name__ == '__main__':
+    main()