blob: 56808f8f6d1672edc4162ad333f1b4150012132e [file] [log] [blame]
#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
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)
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]
scm, nbhd, proj, rest = parts[0], parts[1], parts[2], parts[3:]
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()