diff --git a/.vscode/settings.json b/.vscode/settings.json index 39864dc8..0d15acf1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -55,4 +55,5 @@ "py27" ], "python.linting.enabled": true, + "python.pythonPath": "/usr/bin/python3" } \ No newline at end of file diff --git a/README.md b/README.md index 8dce244b..fcdcc8dc 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,7 @@ permissions: * `w` (write): upload files, move files *into* this folder * `m` (move): move files/folders *from* this folder * `d` (delete): delete files/folders +* `g` (get): only download files, cannot see folder contents or zip/tar examples: * add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3` diff --git a/copyparty/__main__.py b/copyparty/__main__.py index d14ecb7f..de29a921 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -219,6 +219,7 @@ def run_argparse(argv, formatter): "w" (write): upload files; need "r" to see the uploads "m" (move): move files and folders; need "w" at destination "d" (delete): permanently delete files and folders + "g" (get): download files, but cannot see folder contents too many volflags to list here, see the other sections @@ -493,7 +494,7 @@ def main(argv=None): if re.match("c[^,]", opt): mod = True na.append("c," + opt[1:]) - elif re.sub("^[rwmd]*", "", opt) and "," not in opt: + elif re.sub("^[rwmdg]*", "", opt) and "," not in opt: mod = True perm = opt[0] if perm == "a": diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index c58f9f43..296fae07 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -29,17 +29,18 @@ LEELOO_DALLAS = "leeloo_dallas" class AXS(object): - def __init__(self, uread=None, uwrite=None, umove=None, udel=None): + def __init__(self, uread=None, uwrite=None, umove=None, udel=None, uget=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} + self.uget = {} if uget is None else {k: 1 for k in uget} def __repr__(self): return "AXS({})".format( ", ".join( "{}={!r}".format(k, self.__dict__[k]) - for k in "uread uwrite umove udel".split() + for k in "uread uwrite umove udel uget".split() ) ) @@ -215,6 +216,7 @@ class VFS(object): self.awrite = {} self.amove = {} self.adel = {} + self.aget = {} else: self.histpath = None self.all_vols = None @@ -222,6 +224,7 @@ class VFS(object): self.awrite = None self.amove = None self.adel = None + self.aget = None def __repr__(self): return "VFS({})".format( @@ -308,7 +311,7 @@ class VFS(object): def can_access(self, vpath, uname): # type: (str, str) -> tuple[bool, bool, bool, bool] - """can Read,Write,Move,Delete""" + """can Read,Write,Move,Delete,Get""" vn, _ = self._find(vpath) c = vn.axs return [ @@ -316,10 +319,20 @@ class VFS(object): uname in c.uwrite or "*" in c.uwrite, uname in c.umove or "*" in c.umove, uname in c.udel or "*" in c.udel, + uname in c.uget or "*" in c.uget, ] - def get(self, vpath, uname, will_read, will_write, will_move=False, will_del=False): - # type: (str, str, bool, bool, bool, bool) -> tuple[VFS, str] + def get( + self, + vpath, + uname, + will_read, + will_write, + will_move=False, + will_del=False, + will_get=False, + ): + # type: (str, str, bool, bool, bool, bool, bool) -> tuple[VFS, str] """returns [vfsnode,fs_remainder] if user has the requested permissions""" vn, rem = self._find(vpath) c = vn.axs @@ -329,6 +342,7 @@ class VFS(object): [will_write, c.uwrite, "write"], [will_move, c.umove, "move"], [will_del, c.udel, "delete"], + [will_get, c.uget, "get"], ]: if req and (uname not in d and "*" not in d) and uname != LEELOO_DALLAS: m = "you don't have {}-access for this location" @@ -368,7 +382,7 @@ class VFS(object): for name, vn2 in sorted(self.nodes.items()): ok = False axs = vn2.axs - axs = [axs.uread, axs.uwrite, axs.umove, axs.udel] + axs = [axs.uread, axs.uwrite, axs.umove, axs.udel, axs.uget] for pset in permsets: ok = True for req, lst in zip(pset, axs): @@ -561,7 +575,7 @@ class AuthSrv(object): def _read_vol_str(self, lvl, uname, axs, flags): # type: (str, str, AXS, any) -> None - if lvl.strip("crwmd"): + if lvl.strip("crwmdg"): raise Exception("invalid volume flag: {},{}".format(lvl, uname)) if lvl == "c": @@ -588,6 +602,9 @@ class AuthSrv(object): if "d" in lvl: axs.udel[un] = 1 + if "g" in lvl: + axs.uget[un] = 1 + def _read_volflag(self, flags, name, value, is_list): if name not in ["mtp"]: flags[name] = value @@ -625,7 +642,7 @@ class AuthSrv(object): if self.args.v: # list of src:dst:permset:permset:... - # permset is [,username][,username] or ,[=args] + # permset is [,username][,username] or ,[=args] for v_str in self.args.v: m = re_vol.match(v_str) if not m: @@ -692,20 +709,21 @@ class AuthSrv(object): vfs.all_vols = {} vfs.get_all_vols(vfs.all_vols) - for perm in "read write move del".split(): + for perm in "read write move del get".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): + axs = getattr(vol.axs, axs_key) + if usr in axs or "*" in axs: umap[usr].append(mp) setattr(vfs, "a" + perm, umap) all_users = {} missing_users = {} for axs in daxs.values(): - for d in [axs.uread, axs.uwrite, axs.umove, axs.udel]: + for d in [axs.uread, axs.uwrite, axs.umove, axs.udel, axs.uget]: for usr in d.keys(): all_users[usr] = 1 if usr != "*" and usr not in acct: @@ -930,6 +948,7 @@ class AuthSrv(object): [" write", "uwrite"], [" move", "umove"], ["delete", "udel"], + [" get", "uget"], ]: u = list(sorted(getattr(v.axs, attr).keys())) u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u) @@ -997,10 +1016,10 @@ class AuthSrv(object): raise Exception("volume not found: " + v) self.log({"users": users, "vols": vols, "flags": flags}) - m = "/{}: read({}) write({}) move({}) del({})" + m = "/{}: read({}) write({}) move({}) del({}) get({})" for k, v in self.vfs.all_vols.items(): vc = v.axs - self.log(m.format(k, vc.uread, vc.uwrite, vc.umove, vc.udel)) + self.log(m.format(k, vc.uread, vc.uwrite, vc.umove, vc.udel, vc.uget)) flag_v = "v" in flags flag_ln = "ln" in flags @@ -1014,7 +1033,7 @@ class AuthSrv(object): for u in users: self.log("checking /{} as {}".format(v, u)) try: - vn, _ = self.vfs.get(v, u, True, False, False, False) + vn, _ = self.vfs.get(v, u, True, False, False, False, False) except: continue diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index d7f03598..481635d4 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -213,6 +213,7 @@ class HttpCli(object): self.wvol = self.asrv.vfs.awrite[self.uname] self.mvol = self.asrv.vfs.amove[self.uname] self.dvol = self.asrv.vfs.adel[self.uname] + self.gvol = self.asrv.vfs.aget[self.uname] if pwd and "pw" in self.ouparam and pwd != cookies.get("cppwd"): self.out_headers["Set-Cookie"] = self.get_pwd_cookie(pwd)[0] @@ -379,8 +380,8 @@ class HttpCli(object): return self.tx_file(static_path) x = self.asrv.vfs.can_access(self.vpath, self.uname) - self.can_read, self.can_write, self.can_move, self.can_delete = x - if not self.can_read and not self.can_write: + self.can_read, self.can_write, self.can_move, self.can_delete, self.can_get = x + if not self.can_read and not self.can_write and not self.can_get: if self.vpath: self.log("inaccessible: [{}]".format(self.vpath)) raise Pebkac(404) @@ -1784,13 +1785,13 @@ class HttpCli(object): except: raise Pebkac(404) - if self.can_read: - if rem.startswith(".hist/up2k.") or ( - rem.endswith("/dir.txt") and rem.startswith(".hist/th/") - ): - raise Pebkac(403) + if rem.startswith(".hist/up2k.") or ( + rem.endswith("/dir.txt") and rem.startswith(".hist/th/") + ): + raise Pebkac(403) - is_dir = stat.S_ISDIR(st.st_mode) + is_dir = stat.S_ISDIR(st.st_mode) + if self.can_read: th_fmt = self.uparam.get("th") if th_fmt is not None: if is_dir: @@ -1815,11 +1816,11 @@ class HttpCli(object): return self.tx_ico(rem) - if not is_dir: - if abspath.endswith(".md") and "raw" not in self.uparam: - return self.tx_md(abspath) + if not is_dir and (self.can_read or self.can_get): + if abspath.endswith(".md") and "raw" not in self.uparam: + return self.tx_md(abspath) - return self.tx_file(abspath) + return self.tx_file(abspath) srv_info = [] @@ -1859,6 +1860,8 @@ class HttpCli(object): perms.append("move") if self.can_delete: perms.append("delete") + if self.can_get: + perms.append("get") url_suf = self.urlq({}, []) is_ls = "ls" in self.uparam @@ -1928,6 +1931,9 @@ class HttpCli(object): if not stat.S_ISDIR(st.st_mode): raise Pebkac(404) + if "zip" in self.uparam or "tar" in self.uparam: + raise Pebkac(403) + html = self.j2(tpl, **j2a) self.reply(html.encode("utf-8", "replace"), headers=NO_STORE) return True diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 4ba39210..43200bc6 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -227,9 +227,22 @@ function goto(dest) { clmod(obj[a], 'act'); if (dest) { - var ui = ebi('op_' + dest); + var ui = ebi('op_' + dest), + lnk = QS('#ops>a[data-dest=' + dest + ']'), + nps = lnk.getAttribute('data-perm'); + + nps = nps && nps.length ? nps.split(' ') : []; + + if (perms.length) + for (var a = 0; a < nps.length; a++) + if (!has(perms, nps[a])) + return; + + if (!has(perms, 'read') && !has(perms, 'write') && (dest == 'up2k')) + return; + clmod(ui, 'act', true); - QS('#ops>a[data-dest=' + dest + ']').className += " act"; + lnk.className += " act"; var fn = window['goto_' + dest]; if (fn) @@ -3426,7 +3439,7 @@ function apply_perms(newperms) { var axs = [], aclass = '>', - chk = ['read', 'write', 'move', 'delete']; + chk = ['read', 'write', 'move', 'delete', 'get']; for (var a = 0; a < chk.length; a++) if (has(perms, chk[a])) @@ -3480,7 +3493,7 @@ function apply_perms(newperms) { ebi('widget').style.display = have_read ? '' : 'none'; thegrid.setvis(have_read); - if (!have_read) + if (!have_read && have_write) goto('up2k'); } diff --git a/copyparty/web/up2k.js b/copyparty/web/up2k.js index 860ef555..9bb33b56 100644 --- a/copyparty/web/up2k.js +++ b/copyparty/web/up2k.js @@ -528,7 +528,7 @@ function up2k_init(subtle) { got_deps = true; } - if (perms.length && !has(perms, 'read')) + if (perms.length && !has(perms, 'read') && has(perms, 'write')) goto('up2k'); function setmsg(msg, type) { diff --git a/tests/test_httpcli.py b/tests/test_httpcli.py index 669d1fc3..6dff1a26 100644 --- a/tests/test_httpcli.py +++ b/tests/test_httpcli.py @@ -98,7 +98,7 @@ class TestHttpCli(unittest.TestCase): if not vol.startswith(top): continue - mode = vol[-2].replace("a", "rwmd") + mode = vol[-2].replace("a", "rw") usr = vol[-1] if usr == "a": usr = "" diff --git a/tests/test_vfs.py b/tests/test_vfs.py index 05a07866..a3ed1099 100644 --- a/tests/test_vfs.py +++ b/tests/test_vfs.py @@ -197,10 +197,10 @@ class TestVFS(unittest.TestCase): self.assertEqual(n.realpath, os.path.join(td, "a")) self.assertAxs(n.axs.uread, ["*"]) self.assertAxs(n.axs.uwrite, []) - self.assertEqual(vfs.can_access("/", "*"), [False, False, False, False]) - self.assertEqual(vfs.can_access("/", "k"), [True, True, False, False]) - self.assertEqual(vfs.can_access("/a", "*"), [True, False, False, False]) - self.assertEqual(vfs.can_access("/a", "k"), [True, False, False, False]) + self.assertEqual(vfs.can_access("/", "*"), [False, False, False, False, False]) + self.assertEqual(vfs.can_access("/", "k"), [True, True, False, False, False]) + self.assertEqual(vfs.can_access("/a", "*"), [True, False, False, False, False]) + self.assertEqual(vfs.can_access("/a", "k"), [True, False, False, False, False]) # breadth-first construction vfs = AuthSrv(