shadowing: //NULL as unmap keyword

This commit is contained in:
ed 2026-07-09 21:42:59 +00:00
parent 12878e6c1c
commit 8884606138
6 changed files with 66 additions and 8 deletions

View file

@ -616,12 +616,26 @@ hiding specific subfolders by mounting another volume on top of them
for example `-v /mnt::r -v /var/empty:web/certs:` (note: no permissions) mounts the server folder `/mnt` as the webroot, but another volume is mounted at `/web/certs` -- so visitors can only see the contents of `/mnt` and `/mnt/web` (at URLs `/` and `/web`), but not `/mnt/web/certs` because URL `/web/certs` is mapped to `/var/empty`
to fully unmap it from the filesystem, specify `//NULL` instead of a real path such as `/var/empty`, so for example `-v /mnt::r -v //NULL:web/certs:` ensures `/web/certs` will never be accessible by anyone
the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead
so, to shadow a file/folder, define a volume but leave out the `accs:` section
> this also works for single files, because files can also be volumes
config file example for unmapping folders by shadowing:
```yaml
[/drives]
/mnt # url "/drives" goes to "/mnt"
accs:
r: * # everyone can read
[/drives/foo/bar]
//NULL # blocks access to "/mnt/foo/bar"
```
## dotfiles

View file

@ -1195,7 +1195,7 @@ class AuthSrv(object):
for ptn, sigil in ((PTN_U_ANY, "${u}"), (PTN_G_ANY, "${g}")):
if bool(ptn.search(src)) != bool(ptn.search(dst)):
zsl.append(sigil)
if zsl:
if zsl and src != "//NULL":
t = "ERROR: if %s is mentioned in a volume definition, it must be included in both the filesystem-path [%s] and the volume-url [/%s]"
t = "\n".join([t % (x, src, dst) for x in zsl])
self.log(t, 1)
@ -1275,8 +1275,16 @@ class AuthSrv(object):
daxs: dict[str, AXS],
mflags: dict[str, dict[str, Any]],
) -> tuple[str, str]:
src = os.path.expanduser(self.args.shenvexp(src))
src = absreal(src)
if src == "//NULL":
src = ""
else:
src = os.path.expanduser(self.args.shenvexp(src))
src = absreal(src)
if dst == "//NULL":
t = "//NULL given as URL instead of filesystem-path"
self.log(t, 1)
raise Exception(t)
dst = dst.strip("/")
if dst in mount:
@ -1284,7 +1292,7 @@ class AuthSrv(object):
self.log(t.format(dst, mount[dst][0], src), c=1)
raise Exception(BAD_CFG)
if src in mount.values():
if src and src in mount.values():
t = "filesystem-path [{}] mounted in multiple locations:"
t = t.format(src)
for v in [k for k, v in mount.items() if v[0] == src] + [dst]:
@ -1293,7 +1301,7 @@ class AuthSrv(object):
self.log(t, c=3)
raise Exception(BAD_CFG)
if not bos.path.exists(src):
if src and not bos.path.exists(src):
self.log("warning: filesystem-path did not exist: %r" % (src,), 3)
vf = {}
@ -1861,7 +1869,7 @@ class AuthSrv(object):
if WINDOWS:
cased = {}
for vp, (ap, vp0) in mount.items():
cased[vp] = (absreal(ap), vp0)
cased[vp] = (absreal(ap) if ap else "", vp0)
mount = cased

View file

@ -15,3 +15,9 @@
[/vb]
/b
[/x1]
//NULL
[/x2]
//NULL

View file

@ -27,3 +27,9 @@
/c
accs:
r: @ga, uc
[/x1]
//NULL
[/x2]
//NULL

View file

@ -89,12 +89,17 @@ class TestVFS(unittest.TestCase):
self.assertEqual(au.vfs.vpath, "")
self.assertApEq(au.vfs.realpath, "/")
self.assertNodes(au.vfs, ["vb"])
self.assertNodes(au.vfs, ["vb", "x1", "x2"])
self.assertNodes(au.vfs.nodes["vb"], [])
self.assertJump(au.vfs.nodes["x1"], [])
self.assertJump(au.vfs.nodes["x2"], [])
self.assertAxs(au.vfs.axs, [["ua"]])
self.assertAxs(au.vfs.nodes["vb"].axs, [])
self.assertEqual("", au.vfs.nodes["x1"].realpath)
self.assertEqual("", au.vfs.nodes["x2"].realpath)
def test_2(self):
"""
users ua/ub/uc, group ga (ua+ub) in basic combinations
@ -104,15 +109,20 @@ class TestVFS(unittest.TestCase):
self.assertEqual(au.vfs.vpath, "")
self.assertApEq(au.vfs.realpath, "/")
self.assertNodes(au.vfs, ["vb", "vc"])
self.assertNodes(au.vfs, ["vb", "vc", "x1", "x2"])
self.assertNodes(au.vfs.nodes["vb"], [])
self.assertNodes(au.vfs.nodes["vc"], [])
self.assertJump(au.vfs.nodes["x1"], [])
self.assertJump(au.vfs.nodes["x2"], [])
self.assertAxs(au.vfs.axs, [["ua", "ub"]])
self.assertAxsAt(au, "vb", [["ua", "ub"]]) # same as:
self.assertAxs(au.vfs.nodes["vb"].axs, [["ua", "ub"]])
self.assertAxs(au.vfs.nodes["vc"].axs, [["ua", "ub", "uc"]])
self.assertEqual("", au.vfs.nodes["x1"].realpath)
self.assertEqual("", au.vfs.nodes["x2"].realpath)
def test_3(self):
"""
IdP-only; dynamically created volumes for users/groups

View file

@ -248,6 +248,20 @@ class TestVFS(unittest.TestCase):
self.assertEqual(r1, r2)
self.assertEqual(list(v1), list(v2))
# shadowing II
self.wipe_vfs(td)
vfs = AuthSrv(Cfg(v=[".::r", "//NULL:a/ab:", "//NULL:a/ac:"]), self.log).vfs
fsp, r1, v1 = self.ls(vfs, "", "*")
self.assertEqual(fsp, td)
self.assertEqual(r1, ["b", "c"])
self.assertEqual(list(v1), ["a"])
fsp, r1, v1 = self.ls(vfs, "a", "*")
self.assertEqual(fsp, os.path.join(td, "a"))
self.assertEqual(r1, ["aa"])
self.assertEqual(list(v1), []) # ["ab", "ac"]
# config file parser
cfg_path = os.path.join(self.td, "test.cfg")
with open(cfg_path, "wb") as f: