add move/delete permission flags

This commit is contained in:
ed 2021-07-22 23:48:29 +02:00
parent e3684e25f8
commit 5b0605774c
8 changed files with 249 additions and 196 deletions

View file

@ -200,23 +200,23 @@ def run_argparse(argv, formatter):
""" """
-a takes username:password, -a takes username:password,
-v takes src:dst:permset:permset:cflag:cflag:... -v takes src:dst:permset:permset:cflag:cflag:...
where "permset" is accesslevel followed by username (no separator) where "permset" is "accesslevel,username"
and "cflag" is config flags to set on this volume and "cflag" is config flags to set on this volume
list of cflags: list of cflags:
"cnodupe" rejects existing files (instead of symlinking them) "c,nodupe" rejects existing files (instead of symlinking them)
"ce2d" sets -e2d (all -e2* args can be set using ce2* cflags) "c,e2d" sets -e2d (all -e2* args can be set using ce2* cflags)
"cd2t" disables metadata collection, overrides -e2t* "c,d2t" disables metadata collection, overrides -e2t*
"cd2d" disables all database stuff, overrides -e2* "c,d2d" disables all database stuff, overrides -e2*
example:\033[35m example:\033[35m
-a ed:hunter2 -v .::r:aed -v ../inc:dump:w:aed:cnodupe \033[36m -a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe \033[36m
mount current directory at "/" with mount current directory at "/" with
* r (read-only) for everyone * r (read-only) for everyone
* a (read+write) for ed * rw (read+write) for ed
mount ../inc at "/dump" with mount ../inc at "/dump" with
* w (write-only) for everyone * w (write-only) for everyone
* a (read+write) for ed * rw (read+write) for ed
* reject duplicate files \033[0m * reject duplicate files \033[0m
if no accounts or volumes are configured, if no accounts or volumes are configured,
@ -377,6 +377,36 @@ def main(argv=None):
except AssertionError: except AssertionError:
al = run_argparse(argv, Dodge11874) al = run_argparse(argv, Dodge11874)
nstrs = []
anymod = False
for ostr in al.v:
mod = False
oa = ostr.split(":")
na = oa[:2]
for opt in oa[2:]:
if opt and (opt[0] == "a" or (len(opt) > 1 and "," not in opt)):
mod = True
perm = opt[0]
if perm == "a":
perm = "rw"
na.append(perm + "," + opt[1:])
elif opt and opt.startswith("c") and not opt.startswith("c,"):
mod = True
na.append("c," + opt[2:])
else:
na.append(opt)
nstr = ":".join(na)
nstrs.append(nstr if mod else ostr)
if mod:
msg = "\033[1;31mWARNING:\033[0;1m\n -v {} \033[0;33mwas replaced with\033[0;1m\n -v {} \n\033[0m"
lprint(msg.format(ostr, nstr))
anymod = True
if anymod:
al.v = nstrs
time.sleep(2)
# propagate implications # propagate implications
for k1, k2 in IMPLICATIONS: for k1, k2 in IMPLICATIONS:
if getattr(al, k1): if getattr(al, k1):

View file

@ -13,17 +13,31 @@ from .__init__ import WINDOWS
from .util import IMPLICATIONS, uncyg, undot, Pebkac, fsdec, fsenc, statdir from .util import IMPLICATIONS, uncyg, undot, Pebkac, fsdec, fsenc, statdir
class AXS(object):
def __init__(self, uread=None, uwrite=None, umove=None, udel=None):
self.uread = {} if uread is None else {k: 1 for k in uread}
self.uwrite = {} if uwrite is None else {k: 1 for k in uwrite}
self.umove = {} if umove is None else {k: 1 for k in umove}
self.udel = {} if udel is None else {k: 1 for k in udel}
def __repr__(self):
return "AXS({})".format(
", ".join(
"{}={!r}".format(k, self.__dict__[k])
for k in "uread uwrite umove udel".split()
)
)
class VFS(object): class VFS(object):
"""single level in the virtual fs""" """single level in the virtual fs"""
def __init__(self, log, realpath, vpath, uread, uwrite, uadm, flags): def __init__(self, log, realpath, vpath, axs, flags):
self.log = log self.log = log
self.realpath = realpath # absolute path on host filesystem self.realpath = realpath # absolute path on host filesystem
self.vpath = vpath # absolute path in the virtual filesystem self.vpath = vpath # absolute path in the virtual filesystem
self.uread = uread # users who can read this self.axs = axs # type: AXS
self.uwrite = uwrite # users who can write this self.flags = flags # config options
self.uadm = uadm # users who are regular admins
self.flags = flags # config switches
self.nodes = {} # child nodes self.nodes = {} # child nodes
self.histtab = None # all realpath->histpath self.histtab = None # all realpath->histpath
self.dbv = None # closest full/non-jump parent self.dbv = None # closest full/non-jump parent
@ -31,15 +45,23 @@ class VFS(object):
if realpath: if realpath:
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
self.all_vols = {vpath: self} # flattened recursive self.all_vols = {vpath: self} # flattened recursive
self.aread = {}
self.awrite = {}
self.amove = {}
self.adel = {}
else: else:
self.histpath = None self.histpath = None
self.all_vols = None self.all_vols = None
self.aread = None
self.awrite = None
self.amove = None
self.adel = None
def __repr__(self): def __repr__(self):
return "VFS({})".format( return "VFS({})".format(
", ".join( ", ".join(
"{}={!r}".format(k, self.__dict__[k]) "{}={!r}".format(k, self.__dict__[k])
for k in "realpath vpath uread uwrite uadm flags".split() for k in "realpath vpath axs flags".split()
) )
) )
@ -66,9 +88,7 @@ class VFS(object):
self.log, self.log,
os.path.join(self.realpath, name) if self.realpath else None, os.path.join(self.realpath, name) if self.realpath else None,
"{}/{}".format(self.vpath, name).lstrip("/"), "{}/{}".format(self.vpath, name).lstrip("/"),
self.uread, self.axs,
self.uwrite,
self.uadm,
self._copy_flags(name), self._copy_flags(name),
) )
vn.dbv = self.dbv or self vn.dbv = self.dbv or self
@ -81,7 +101,7 @@ class VFS(object):
# leaf does not exist; create and keep permissions blank # leaf does not exist; create and keep permissions blank
vp = "{}/{}".format(self.vpath, dst).lstrip("/") vp = "{}/{}".format(self.vpath, dst).lstrip("/")
vn = VFS(self.log, src, vp, [], [], [], {}) vn = VFS(self.log, src, vp, AXS(), {})
vn.dbv = self.dbv or self vn.dbv = self.dbv or self
self.nodes[dst] = vn self.nodes[dst] = vn
return vn return vn
@ -121,23 +141,32 @@ class VFS(object):
return [self, vpath] return [self, vpath]
def can_access(self, vpath, uname): def can_access(self, vpath, uname):
"""return [readable,writable]""" # type: (str, str) -> tuple[bool, bool, bool, bool]
"""can Read,Write,Move,Delete"""
vn, _ = self._find(vpath) vn, _ = self._find(vpath)
c = vn.axs
return [ return [
uname in vn.uread or "*" in vn.uread, uname in c.uread or "*" in c.uread,
uname in vn.uwrite or "*" in vn.uwrite, uname in c.uwrite or "*" in c.uwrite,
uname in c.umove or "*" in c.umove,
uname in c.udel or "*" in c.udel,
] ]
def get(self, vpath, uname, will_read, will_write): def get(self, vpath, uname, will_read, will_write, will_move=False, will_del=False):
# type: (str, str, bool, bool) -> tuple[VFS, str] # type: (str, str, bool, bool, bool, bool) -> tuple[VFS, str]
"""returns [vfsnode,fs_remainder] if user has the requested permissions""" """returns [vfsnode,fs_remainder] if user has the requested permissions"""
vn, rem = self._find(vpath) vn, rem = self._find(vpath)
c = vn.axs
if will_read and (uname not in vn.uread and "*" not in vn.uread): for req, d, msg in [
raise Pebkac(403, "you don't have read-access for this location") [will_read, c.uread, "read"],
[will_write, c.uwrite, "write"],
if will_write and (uname not in vn.uwrite and "*" not in vn.uwrite): [will_move, c.umove, "move"],
raise Pebkac(403, "you don't have write-access for this location") [will_del, c.udel, "delete"],
]:
if req and (uname not in d and "*" not in d):
m = "you don't have {}-access for this location"
raise Pebkac(403, m.format(msg))
return vn, rem return vn, rem
@ -187,10 +216,10 @@ class VFS(object):
real.sort() real.sort()
if not rem: if not rem:
for name, vn2 in sorted(self.nodes.items()): for name, vn2 in sorted(self.nodes.items()):
ok = uname in vn2.uread or "*" in vn2.uread ok = uname in vn2.axs.uread or "*" in vn2.axs.uread
if not ok and incl_wo: if not ok and incl_wo:
ok = uname in vn2.uwrite or "*" in vn2.uwrite ok = uname in vn2.axs.uwrite or "*" in vn2.axs.uwrite
if ok: if ok:
virt_vis[name] = vn2 virt_vis[name] = vn2
@ -295,20 +324,6 @@ class VFS(object):
for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in files]: for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in files]:
yield f yield f
def user_tree(self, uname, readable, writable, admin):
is_readable = False
if uname in self.uread or "*" in self.uread:
readable.append(self.vpath)
is_readable = True
if uname in self.uwrite or "*" in self.uwrite:
writable.append(self.vpath)
if is_readable:
admin.append(self.vpath)
for _, vn in sorted(self.nodes.items()):
vn.user_tree(uname, readable, writable, admin)
class AuthSrv(object): class AuthSrv(object):
"""verifies users against given paths""" """verifies users against given paths"""
@ -341,7 +356,8 @@ class AuthSrv(object):
yield prev, True yield prev, True
def _parse_config_file(self, fd, user, mread, mwrite, madm, mflags, mount): def _parse_config_file(self, fd, acct, daxs, mflags, mount):
# type: (any, str, dict[str, AXS], any, str) -> None
vol_src = None vol_src = None
vol_dst = None vol_dst = None
self.line_ctr = 0 self.line_ctr = 0
@ -357,7 +373,7 @@ class AuthSrv(object):
if vol_src is None: if vol_src is None:
if ln.startswith("u "): if ln.startswith("u "):
u, p = ln[2:].split(":", 1) u, p = ln[2:].split(":", 1)
user[u] = p acct[u] = p
else: else:
vol_src = ln vol_src = ln
continue continue
@ -371,47 +387,46 @@ class AuthSrv(object):
vol_src = fsdec(os.path.abspath(fsenc(vol_src))) vol_src = fsdec(os.path.abspath(fsenc(vol_src)))
vol_dst = vol_dst.strip("/") vol_dst = vol_dst.strip("/")
mount[vol_dst] = vol_src mount[vol_dst] = vol_src
mread[vol_dst] = [] daxs[vol_dst] = AXS()
mwrite[vol_dst] = []
madm[vol_dst] = []
mflags[vol_dst] = {} mflags[vol_dst] = {}
continue continue
if len(ln) > 1: try:
lvl, uname = ln.split(" ") lvl, uname = ln.split(" ", 1)
else: except:
lvl = ln lvl = ln
uname = "*" uname = "*"
self._read_vol_str( if lvl == "a":
lvl, m = "WARNING (config-file): permission flag 'a' is deprecated; please use 'rw' instead"
uname, self.log(m, 1)
mread[vol_dst],
mwrite[vol_dst],
madm[vol_dst],
mflags[vol_dst],
)
def _read_vol_str(self, lvl, uname, mr, mw, ma, mf): self._read_vol_str(lvl, uname, daxs[vol_dst], mflags[vol_dst])
def _read_vol_str(self, lvl, uname, axs, flags):
# type: (str, str, AXS, any) -> None
if lvl == "c": if lvl == "c":
cval = True cval = True
if "=" in uname: if "=" in uname:
uname, cval = uname.split("=", 1) uname, cval = uname.split("=", 1)
self._read_volflag(mf, uname, cval, False) self._read_volflag(flags, uname, cval, False)
return return
if uname == "": if uname == "":
uname = "*" uname = "*"
if lvl in "ra": if "r" in lvl:
mr.append(uname) axs.uread[uname] = 1
if lvl in "wa": if "w" in lvl:
mw.append(uname) axs.uwrite[uname] = 1
if lvl == "a": if "m" in lvl:
ma.append(uname) axs.umove[uname] = 1
if "d" in lvl:
axs.udel[uname] = 1
def _read_volflag(self, flags, name, value, is_list): def _read_volflag(self, flags, name, value, is_list):
if name not in ["mtp"]: if name not in ["mtp"]:
@ -433,21 +448,19 @@ class AuthSrv(object):
before finally building the VFS before finally building the VFS
""" """
user = {} # username:password acct = {} # username:password
mread = {} # mountpoint:[username] daxs = {} # type: dict[str, AXS]
mwrite = {} # mountpoint:[username]
madm = {} # mountpoint:[username]
mflags = {} # mountpoint:[flag] mflags = {} # mountpoint:[flag]
mount = {} # dst:src (mountpoint:realpath) mount = {} # dst:src (mountpoint:realpath)
if self.args.a: if self.args.a:
# list of username:password # list of username:password
for u, p in [x.split(":", 1) for x in self.args.a]: for u, p in [x.split(":", 1) for x in self.args.a]:
user[u] = p acct[u] = p
if self.args.v: if self.args.v:
# list of src:dst:permset:permset:... # list of src:dst:permset:permset:...
# permset is [rwa]username or [c]flag # permset is <rwmd>[,username][,username] or <c>,<flag>[=args]
for v_str in self.args.v: for v_str in self.args.v:
m = self.re_vol.match(v_str) m = self.re_vol.match(v_str)
if not m: if not m:
@ -461,24 +474,18 @@ class AuthSrv(object):
src = fsdec(os.path.abspath(fsenc(src))) src = fsdec(os.path.abspath(fsenc(src)))
dst = dst.strip("/") dst = dst.strip("/")
mount[dst] = src mount[dst] = src
mread[dst] = [] daxs[dst] = AXS()
mwrite[dst] = []
madm[dst] = []
mflags[dst] = {} mflags[dst] = {}
perms = perms.split(":") for x in perms.split(":"):
for (lvl, uname) in [[x[0], x[1:]] for x in perms]: lvl, uname = x.split(",", 1) if "," in x else [x, ""]
self._read_vol_str( self._read_vol_str(lvl, uname, daxs[dst], mflags[dst])
lvl, uname, mread[dst], mwrite[dst], madm[dst], mflags[dst]
)
if self.args.c: if self.args.c:
for cfg_fn in self.args.c: for cfg_fn in self.args.c:
with open(cfg_fn, "rb") as f: with open(cfg_fn, "rb") as f:
try: try:
self._parse_config_file( self._parse_config_file(f, acct, daxs, mflags, mount)
f, user, mread, mwrite, madm, mflags, mount
)
except: except:
m = "\n\033[1;31m\nerror in config file {} on line {}:\n\033[0m" m = "\n\033[1;31m\nerror in config file {} on line {}:\n\033[0m"
self.log(m.format(cfg_fn, self.line_ctr), 1) self.log(m.format(cfg_fn, self.line_ctr), 1)
@ -497,10 +504,11 @@ class AuthSrv(object):
if not mount: if not mount:
# -h says our defaults are CWD at root and read/write for everyone # -h says our defaults are CWD at root and read/write for everyone
vfs = VFS(self.log_func, os.path.abspath("."), "", ["*"], ["*"], ["*"], {}) axs = AXS(["*"], ["*"], None, None)
vfs = VFS(self.log_func, os.path.abspath("."), "", axs, {})
elif "" not in mount: elif "" not in mount:
# there's volumes but no root; make root inaccessible # there's volumes but no root; make root inaccessible
vfs = VFS(self.log_func, None, "", [], [], [], {}) vfs = VFS(self.log_func, None, "", AXS(), {})
vfs.flags["d2d"] = True vfs.flags["d2d"] = True
maxdepth = 0 maxdepth = 0
@ -511,32 +519,34 @@ class AuthSrv(object):
if dst == "": if dst == "":
# rootfs was mapped; fully replaces the default CWD vfs # rootfs was mapped; fully replaces the default CWD vfs
vfs = VFS( vfs = VFS(self.log_func, mount[dst], dst, daxs[dst], mflags[dst])
self.log_func,
mount[dst],
dst,
mread[dst],
mwrite[dst],
madm[dst],
mflags[dst],
)
continue continue
v = vfs.add(mount[dst], dst) v = vfs.add(mount[dst], dst)
v.uread = mread[dst] v.axs = daxs[dst]
v.uwrite = mwrite[dst]
v.uadm = madm[dst]
v.flags = mflags[dst] v.flags = mflags[dst]
v.dbv = None v.dbv = None
vfs.all_vols = {} vfs.all_vols = {}
vfs.get_all_vols(vfs.all_vols) vfs.get_all_vols(vfs.all_vols)
for perm in "read write move del".split():
axs_key = "u" + perm
unames = ["*"] + list(acct.keys())
umap = {x: [] for x in unames}
for usr in unames:
for mp, vol in vfs.all_vols.items():
if usr in getattr(vol.axs, axs_key):
umap[usr].append(mp)
setattr(vfs, "a" + perm, umap)
all_users = {}
missing_users = {} missing_users = {}
for d in [mread, mwrite]: for axs in daxs.values():
for _, ul in d.items(): for d in [axs.uread, axs.uwrite, axs.umove, axs.udel]:
for usr in ul: for usr in d.keys():
if usr != "*" and usr not in user: all_users[usr] = 1
if usr != "*" and usr not in acct:
missing_users[usr] = 1 missing_users[usr] = 1
if missing_users: if missing_users:
@ -611,7 +621,7 @@ class AuthSrv(object):
all_mte = {} all_mte = {}
errors = False errors = False
for vol in vfs.all_vols.values(): for vol in vfs.all_vols.values():
if (self.args.e2ds and vol.uwrite) or self.args.e2dsa: if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
vol.flags["e2ds"] = True vol.flags["e2ds"] = True
if self.args.e2d or "e2ds" in vol.flags: if self.args.e2d or "e2ds" in vol.flags:
@ -711,17 +721,14 @@ class AuthSrv(object):
with self.mutex: with self.mutex:
self.vfs = vfs self.vfs = vfs
self.user = user self.acct = acct
self.iuser = {v: k for k, v in user.items()} self.iacct = {v: k for k, v in acct.items()}
self.re_pwd = None self.re_pwd = None
pwds = [re.escape(x) for x in self.iuser.keys()] pwds = [re.escape(x) for x in self.iacct.keys()]
if pwds: if pwds:
self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)") self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)")
# import pprint
# pprint.pprint({"usr": user, "rd": mread, "wr": mwrite, "mnt": mount})
def dbg_ls(self): def dbg_ls(self):
users = self.args.ls users = self.args.ls
vols = "*" vols = "*"
@ -739,12 +746,12 @@ class AuthSrv(object):
pass pass
if users == "**": if users == "**":
users = list(self.user.keys()) + ["*"] users = list(self.acct.keys()) + ["*"]
else: else:
users = [users] users = [users]
for u in users: for u in users:
if u not in self.user and u != "*": if u not in self.acct and u != "*":
raise Exception("user not found: " + u) raise Exception("user not found: " + u)
if vols == "*": if vols == "*":
@ -760,8 +767,10 @@ class AuthSrv(object):
raise Exception("volume not found: " + v) raise Exception("volume not found: " + v)
self.log({"users": users, "vols": vols, "flags": flags}) self.log({"users": users, "vols": vols, "flags": flags})
m = "/{}: read({}) write({}) move({}) del({})"
for k, v in self.vfs.all_vols.items(): for k, v in self.vfs.all_vols.items():
self.log("/{}: read({}) write({})".format(k, v.uread, v.uwrite)) vc = v.axs
self.log(m.format(k, vc.uread, vc.uwrite, vc.umove, vc.udel))
flag_v = "v" in flags flag_v = "v" in flags
flag_ln = "ln" in flags flag_ln = "ln" in flags
@ -775,7 +784,7 @@ class AuthSrv(object):
for u in users: for u in users:
self.log("checking /{} as {}".format(v, u)) self.log("checking /{} as {}".format(v, u))
try: try:
vn, _ = self.vfs.get(v, u, True, False) vn, _ = self.vfs.get(v, u, True, False, False, False)
except: except:
continue continue

View file

@ -58,7 +58,7 @@ class HttpCli(object):
def unpwd(self, m): def unpwd(self, m):
a, b = m.groups() a, b = m.groups()
return "=\033[7m {} \033[27m{}".format(self.asrv.iuser[a], b) return "=\033[7m {} \033[27m{}".format(self.asrv.iacct[a], b)
def _check_nonfatal(self, ex): def _check_nonfatal(self, ex):
return ex.code < 400 or ex.code in [404, 429] return ex.code < 400 or ex.code in [404, 429]
@ -181,9 +181,11 @@ class HttpCli(object):
self.vpath = unquotep(vpath) self.vpath = unquotep(vpath)
pwd = uparam.get("pw") pwd = uparam.get("pw")
self.uname = self.asrv.iuser.get(pwd, "*") self.uname = self.asrv.iacct.get(pwd, "*")
self.rvol, self.wvol, self.avol = [[], [], []] self.rvol = self.asrv.vfs.aread[self.uname]
self.asrv.vfs.user_tree(self.uname, self.rvol, self.wvol, self.avol) self.wvol = self.asrv.vfs.awrite[self.uname]
self.mvol = self.asrv.vfs.amove[self.uname]
self.dvol = self.asrv.vfs.adel[self.uname]
if pwd and "pw" in self.ouparam and pwd != cookies.get("cppwd"): if pwd and "pw" in self.ouparam and pwd != cookies.get("cppwd"):
self.out_headers["Set-Cookie"] = self.get_pwd_cookie(pwd)[0] self.out_headers["Set-Cookie"] = self.get_pwd_cookie(pwd)[0]
@ -359,8 +361,9 @@ class HttpCli(object):
self.redirect(vpath, flavor="redirecting to", use302=True) self.redirect(vpath, flavor="redirecting to", use302=True)
return True return True
self.readable, self.writable = self.asrv.vfs.can_access(self.vpath, self.uname) x = self.asrv.vfs.can_access(self.vpath, self.uname)
if not self.readable and not self.writable: self.can_read, self.can_write, self.can_move, self.can_delete = x
if not self.can_read and not self.can_write:
if self.vpath: if self.vpath:
self.log("inaccessible: [{}]".format(self.vpath)) self.log("inaccessible: [{}]".format(self.vpath))
raise Pebkac(404) raise Pebkac(404)
@ -775,7 +778,7 @@ class HttpCli(object):
return True return True
def get_pwd_cookie(self, pwd): def get_pwd_cookie(self, pwd):
if pwd in self.asrv.iuser: if pwd in self.asrv.iacct:
msg = "login ok" msg = "login ok"
dt = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24 * 365) dt = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24 * 365)
exp = dt.strftime("%a, %d %b %Y %H:%M:%S GMT") exp = dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
@ -994,13 +997,6 @@ class HttpCli(object):
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True) vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
self._assert_safe_rem(rem) self._assert_safe_rem(rem)
# TODO:
# the per-volume read/write permissions must be replaced with permission flags
# which would decide how to handle uploads to filenames which are taken,
# current behavior of creating a new name is a good default for binary files
# but should also offer a flag to takeover the filename and rename the old one
#
# stopgap:
if not rem.endswith(".md"): if not rem.endswith(".md"):
raise Pebkac(400, "only markdown pls") raise Pebkac(400, "only markdown pls")
@ -1051,7 +1047,6 @@ class HttpCli(object):
self.reply(response.encode("utf-8")) self.reply(response.encode("utf-8"))
return True return True
# TODO another hack re: pending permissions rework
mdir, mfile = os.path.split(fp) mdir, mfile = os.path.split(fp)
mfile2 = "{}.{:.3f}.md".format(mfile[:-3], srv_lastmod) mfile2 = "{}.{:.3f}.md".format(mfile[:-3], srv_lastmod)
try: try:
@ -1424,12 +1419,13 @@ class HttpCli(object):
def tx_mounts(self): def tx_mounts(self):
suf = self.urlq({}, ["h"]) suf = self.urlq({}, ["h"])
avol = [x for x in self.wvol if x in self.rvol]
rvol, wvol, avol = [ rvol, wvol, avol = [
[("/" + x).rstrip("/") + "/" for x in y] [("/" + x).rstrip("/") + "/" for x in y]
for y in [self.rvol, self.wvol, self.avol] for y in [self.rvol, self.wvol, avol]
] ]
if self.avol and not self.args.no_rescan: if avol and not self.args.no_rescan:
x = self.conn.hsrv.broker.put(True, "up2k.get_state") x = self.conn.hsrv.broker.put(True, "up2k.get_state")
vs = json.loads(x.get()) vs = json.loads(x.get())
vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()} vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()}
@ -1454,7 +1450,7 @@ class HttpCli(object):
return True return True
def scanvol(self): def scanvol(self):
if not self.readable or not self.writable: if not self.can_read or not self.can_write:
raise Pebkac(403, "not admin") raise Pebkac(403, "not admin")
if self.args.no_rescan: if self.args.no_rescan:
@ -1473,7 +1469,7 @@ class HttpCli(object):
raise Pebkac(500, x) raise Pebkac(500, x)
def tx_stack(self): def tx_stack(self):
if not self.avol: if not [x for x in self.wvol if x in self.rvol]:
raise Pebkac(403, "not admin") raise Pebkac(403, "not admin")
if self.args.no_stack: if self.args.no_stack:
@ -1551,9 +1547,7 @@ class HttpCli(object):
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)]) vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
vn, rem = self.asrv.vfs.get( vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
self.vpath, self.uname, self.readable, self.writable
)
abspath = vn.canonical(rem) abspath = vn.canonical(rem)
dbv, vrem = vn.get_dbv(rem) dbv, vrem = vn.get_dbv(rem)
@ -1562,7 +1556,7 @@ class HttpCli(object):
except: except:
raise Pebkac(404) raise Pebkac(404)
if self.readable: if self.can_read:
if rem.startswith(".hist/up2k.") or ( if rem.startswith(".hist/up2k.") or (
rem.endswith("/dir.txt") and rem.startswith(".hist/th/") rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
): ):
@ -1629,10 +1623,14 @@ class HttpCli(object):
srv_info = "</span> /// <span>".join(srv_info) srv_info = "</span> /// <span>".join(srv_info)
perms = [] perms = []
if self.readable: if self.can_read:
perms.append("read") perms.append("read")
if self.writable: if self.can_write:
perms.append("write") perms.append("write")
if self.can_move:
perms.append("move")
if self.can_delete:
perms.append("delete")
url_suf = self.urlq({}, []) url_suf = self.urlq({}, [])
is_ls = "ls" in self.uparam is_ls = "ls" in self.uparam
@ -1668,13 +1666,13 @@ class HttpCli(object):
"have_up2k_idx": ("e2d" in vn.flags), "have_up2k_idx": ("e2d" in vn.flags),
"have_tags_idx": ("e2t" in vn.flags), "have_tags_idx": ("e2t" in vn.flags),
"have_zip": (not self.args.no_zip), "have_zip": (not self.args.no_zip),
"have_b_u": (self.writable and self.uparam.get("b") == "u"), "have_b_u": (self.can_write and self.uparam.get("b") == "u"),
"url_suf": url_suf, "url_suf": url_suf,
"logues": logues, "logues": logues,
"title": html_escape(self.vpath, crlf=True), "title": html_escape(self.vpath, crlf=True),
"srv_info": srv_info, "srv_info": srv_info,
} }
if not self.readable: if not self.can_read:
if is_ls: if is_ls:
ret = json.dumps(ls_ret) ret = json.dumps(ls_ret)
self.reply( self.reply(

View file

@ -229,21 +229,13 @@ a, #files tbody div a:last-child {
right: 2em; right: 2em;
color: #999; color: #999;
} }
#acc_info span:before { #acc_info span {
color: #f4c; color: #999;
border-bottom: 1px solid rgba(255,68,204,0.6);
margin-right: .6em; margin-right: .6em;
} }
html.read #acc_info span:before { #acc_info span.warn {
content: 'Read-Only access'; color: #f4c;
} border-bottom: 1px solid rgba(255,68,204,0.6);
html.write #acc_info span:before {
content: 'Write-Only access';
}
html.read.write #acc_info span:before {
content: 'Read-Write access';
color: #999;
border: none;
} }
#files tbody a.play { #files tbody a.play {
color: #e70; color: #e70;

View file

@ -2558,9 +2558,22 @@ function despin(sel) {
function apply_perms(newperms) { function apply_perms(newperms) {
perms = newperms || []; perms = newperms || [];
ebi('acc_info').innerHTML = '<span>' + (acct != '*' ? var axs = [],
'<a href="/?pw=x">Logout ' + acct + '</a>' : aclass = '>',
'<a href="/?h">Login</a>') + '</span>'; chk = ['read', 'write', 'rename', 'delete'];
for (var a = 0; a < chk.length; a++)
if (has(perms, chk[a]))
axs.push(chk[a].slice(0, 1).toUpperCase() + chk[a].slice(1));
axs = axs.join('-');
if (perms.length == 1) {
aclass = ' class="warn">';
axs += '-Only';
}
ebi('acc_info').innerHTML = '<span' + aclass + axs + ' access</span>' + (acct != '*' ?
'<a href="/?pw=x">Logout ' + acct + '</a>' : '<a href="/?h">Login</a>');
var o = QSA('#ops>a[data-perm], #u2footfoot'); var o = QSA('#ops>a[data-perm], #u2footfoot');
for (var a = 0; a < o.length; a++) { for (var a = 0; a < o.length; a++) {

View file

@ -10,19 +10,25 @@ u k:k
# share "." (the current directory) # share "." (the current directory)
# as "/" (the webroot) for the following users: # as "/" (the webroot) for the following users:
# "r" grants read-access for anyone # "r" grants read-access for anyone
# "a ed" grants read-write to ed # "rw ed" grants read-write to ed
. .
/ /
r r
a ed rw ed
# custom permissions for the "priv" folder: # custom permissions for the "priv" folder:
# user "k" can see/read the contents # user "k" can only see/read the contents
# and "ed" gets read-write access # user "ed" gets read-write access
./priv ./priv
/priv /priv
r k r k
a ed rw ed
# this does the same thing:
./priv
/priv
r ed k
w ed
# share /home/ed/Music/ as /music and let anyone read it # share /home/ed/Music/ as /music and let anyone read it
# (this will replace any folder called "music" in the webroot) # (this will replace any folder called "music" in the webroot)
@ -41,5 +47,5 @@ c e2d
c nodupe c nodupe
# this entire config file can be replaced with these arguments: # this entire config file can be replaced with these arguments:
# -u ed:123 -u k:k -v .::r:aed -v priv:priv:rk:aed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w # -u ed:123 -u k:k -v .::r:a,ed -v priv:priv:r,k:rw,ed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w:c,e2d:c,nodupe
# but note that the config file always wins in case of conflicts # but note that the config file always wins in case of conflicts

View file

@ -90,7 +90,7 @@ class TestHttpCli(unittest.TestCase):
if not vol.startswith(top): if not vol.startswith(top):
continue continue
mode = vol[-2] mode = vol[-2].replace("a", "rw")
usr = vol[-1] usr = vol[-1]
if usr == "a": if usr == "a":
usr = "" usr = ""
@ -99,7 +99,7 @@ class TestHttpCli(unittest.TestCase):
vol += "/" vol += "/"
top, sub = vol.split("/", 1) top, sub = vol.split("/", 1)
vcfg.append("{0}/{1}:{1}:{2}{3}".format(top, sub, mode, usr)) vcfg.append("{0}/{1}:{1}:{2},{3}".format(top, sub, mode, usr))
pprint.pprint(vcfg) pprint.pprint(vcfg)

View file

@ -68,6 +68,11 @@ class TestVFS(unittest.TestCase):
def log(self, src, msg, c=0): def log(self, src, msg, c=0):
pass pass
def assertAxs(self, dct, lst):
t1 = list(sorted(dct.keys()))
t2 = list(sorted(lst))
self.assertEqual(t1, t2)
def test(self): def test(self):
td = os.path.join(self.td, "vfs") td = os.path.join(self.td, "vfs")
os.mkdir(td) os.mkdir(td)
@ -88,53 +93,53 @@ class TestVFS(unittest.TestCase):
self.assertEqual(vfs.nodes, {}) self.assertEqual(vfs.nodes, {})
self.assertEqual(vfs.vpath, "") self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, td) self.assertEqual(vfs.realpath, td)
self.assertEqual(vfs.uread, ["*"]) self.assertAxs(vfs.axs.uread, ["*"])
self.assertEqual(vfs.uwrite, ["*"]) self.assertAxs(vfs.axs.uwrite, ["*"])
# single read-only rootfs (relative path) # single read-only rootfs (relative path)
vfs = AuthSrv(Cfg(v=["a/ab/::r"]), self.log).vfs vfs = AuthSrv(Cfg(v=["a/ab/::r"]), self.log).vfs
self.assertEqual(vfs.nodes, {}) self.assertEqual(vfs.nodes, {})
self.assertEqual(vfs.vpath, "") self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, os.path.join(td, "a", "ab")) self.assertEqual(vfs.realpath, os.path.join(td, "a", "ab"))
self.assertEqual(vfs.uread, ["*"]) self.assertAxs(vfs.axs.uread, ["*"])
self.assertEqual(vfs.uwrite, []) self.assertAxs(vfs.axs.uwrite, [])
# single read-only rootfs (absolute path) # single read-only rootfs (absolute path)
vfs = AuthSrv(Cfg(v=[td + "//a/ac/../aa//::r"]), self.log).vfs vfs = AuthSrv(Cfg(v=[td + "//a/ac/../aa//::r"]), self.log).vfs
self.assertEqual(vfs.nodes, {}) self.assertEqual(vfs.nodes, {})
self.assertEqual(vfs.vpath, "") self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, os.path.join(td, "a", "aa")) self.assertEqual(vfs.realpath, os.path.join(td, "a", "aa"))
self.assertEqual(vfs.uread, ["*"]) self.assertAxs(vfs.axs.uread, ["*"])
self.assertEqual(vfs.uwrite, []) self.assertAxs(vfs.axs.uwrite, [])
# read-only rootfs with write-only subdirectory (read-write for k) # read-only rootfs with write-only subdirectory (read-write for k)
vfs = AuthSrv( vfs = AuthSrv(
Cfg(a=["k:k"], v=[".::r:ak", "a/ac/acb:a/ac/acb:w:ak"]), Cfg(a=["k:k"], v=[".::r:rw,k", "a/ac/acb:a/ac/acb:w:rw,k"]),
self.log, self.log,
).vfs ).vfs
self.assertEqual(len(vfs.nodes), 1) self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(vfs.vpath, "") self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, td) self.assertEqual(vfs.realpath, td)
self.assertEqual(vfs.uread, ["*", "k"]) self.assertAxs(vfs.axs.uread, ["*", "k"])
self.assertEqual(vfs.uwrite, ["k"]) self.assertAxs(vfs.axs.uwrite, ["k"])
n = vfs.nodes["a"] n = vfs.nodes["a"]
self.assertEqual(len(vfs.nodes), 1) self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(n.vpath, "a") self.assertEqual(n.vpath, "a")
self.assertEqual(n.realpath, os.path.join(td, "a")) self.assertEqual(n.realpath, os.path.join(td, "a"))
self.assertEqual(n.uread, ["*", "k"]) self.assertAxs(n.axs.uread, ["*", "k"])
self.assertEqual(n.uwrite, ["k"]) self.assertAxs(n.axs.uwrite, ["k"])
n = n.nodes["ac"] n = n.nodes["ac"]
self.assertEqual(len(vfs.nodes), 1) self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(n.vpath, "a/ac") self.assertEqual(n.vpath, "a/ac")
self.assertEqual(n.realpath, os.path.join(td, "a", "ac")) self.assertEqual(n.realpath, os.path.join(td, "a", "ac"))
self.assertEqual(n.uread, ["*", "k"]) self.assertAxs(n.axs.uread, ["*", "k"])
self.assertEqual(n.uwrite, ["k"]) self.assertAxs(n.axs.uwrite, ["k"])
n = n.nodes["acb"] n = n.nodes["acb"]
self.assertEqual(n.nodes, {}) self.assertEqual(n.nodes, {})
self.assertEqual(n.vpath, "a/ac/acb") self.assertEqual(n.vpath, "a/ac/acb")
self.assertEqual(n.realpath, os.path.join(td, "a", "ac", "acb")) self.assertEqual(n.realpath, os.path.join(td, "a", "ac", "acb"))
self.assertEqual(n.uread, ["k"]) self.assertAxs(n.axs.uread, ["k"])
self.assertEqual(n.uwrite, ["*", "k"]) self.assertAxs(n.axs.uwrite, ["*", "k"])
# something funky about the windows path normalization, # something funky about the windows path normalization,
# doesn't really matter but makes the test messy, TODO? # doesn't really matter but makes the test messy, TODO?
@ -173,24 +178,24 @@ class TestVFS(unittest.TestCase):
# admin-only rootfs with all-read-only subfolder # admin-only rootfs with all-read-only subfolder
vfs = AuthSrv( vfs = AuthSrv(
Cfg(a=["k:k"], v=[".::ak", "a:a:r"]), Cfg(a=["k:k"], v=[".::rw,k", "a:a:r"]),
self.log, self.log,
).vfs ).vfs
self.assertEqual(len(vfs.nodes), 1) self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(vfs.vpath, "") self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, td) self.assertEqual(vfs.realpath, td)
self.assertEqual(vfs.uread, ["k"]) self.assertAxs(vfs.axs.uread, ["k"])
self.assertEqual(vfs.uwrite, ["k"]) self.assertAxs(vfs.axs.uwrite, ["k"])
n = vfs.nodes["a"] n = vfs.nodes["a"]
self.assertEqual(len(vfs.nodes), 1) self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(n.vpath, "a") self.assertEqual(n.vpath, "a")
self.assertEqual(n.realpath, os.path.join(td, "a")) self.assertEqual(n.realpath, os.path.join(td, "a"))
self.assertEqual(n.uread, ["*"]) self.assertAxs(n.axs.uread, ["*"])
self.assertEqual(n.uwrite, []) self.assertAxs(n.axs.uwrite, [])
self.assertEqual(vfs.can_access("/", "*"), [False, False]) self.assertEqual(vfs.can_access("/", "*"), [False, False, False, False])
self.assertEqual(vfs.can_access("/", "k"), [True, True]) self.assertEqual(vfs.can_access("/", "k"), [True, True, False, False])
self.assertEqual(vfs.can_access("/a", "*"), [True, False]) self.assertEqual(vfs.can_access("/a", "*"), [True, False, False, False])
self.assertEqual(vfs.can_access("/a", "k"), [True, False]) self.assertEqual(vfs.can_access("/a", "k"), [True, False, False, False])
# breadth-first construction # breadth-first construction
vfs = AuthSrv( vfs = AuthSrv(
@ -247,26 +252,26 @@ class TestVFS(unittest.TestCase):
./src ./src
/dst /dst
r a r a
a asd rw asd
""" """
).encode("utf-8") ).encode("utf-8")
) )
au = AuthSrv(Cfg(c=[cfg_path]), self.log) au = AuthSrv(Cfg(c=[cfg_path]), self.log)
self.assertEqual(au.user["a"], "123") self.assertEqual(au.acct["a"], "123")
self.assertEqual(au.user["asd"], "fgh:jkl") self.assertEqual(au.acct["asd"], "fgh:jkl")
n = au.vfs n = au.vfs
# root was not defined, so PWD with no access to anyone # root was not defined, so PWD with no access to anyone
self.assertEqual(n.vpath, "") self.assertEqual(n.vpath, "")
self.assertEqual(n.realpath, None) self.assertEqual(n.realpath, None)
self.assertEqual(n.uread, []) self.assertAxs(n.axs.uread, [])
self.assertEqual(n.uwrite, []) self.assertAxs(n.axs.uwrite, [])
self.assertEqual(len(n.nodes), 1) self.assertEqual(len(n.nodes), 1)
n = n.nodes["dst"] n = n.nodes["dst"]
self.assertEqual(n.vpath, "dst") self.assertEqual(n.vpath, "dst")
self.assertEqual(n.realpath, os.path.join(td, "src")) self.assertEqual(n.realpath, os.path.join(td, "src"))
self.assertEqual(n.uread, ["a", "asd"]) self.assertAxs(n.axs.uread, ["a", "asd"])
self.assertEqual(n.uwrite, ["asd"]) self.assertAxs(n.axs.uwrite, ["asd"])
self.assertEqual(len(n.nodes), 0) self.assertEqual(len(n.nodes), 0)
os.unlink(cfg_path) os.unlink(cfg_path)