Skip to content

Commit f195998

Browse files
committed
per-volume uid/gid; closes #265
1 parent a9d07c6 commit f195998

File tree

13 files changed

+120
-51
lines changed

13 files changed

+120
-51
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ made in Norway 🇳🇴
8080
* [periodic rescan](#periodic-rescan) - filesystem monitoring
8181
* [upload rules](#upload-rules) - set upload rules using volflags
8282
* [compress uploads](#compress-uploads) - files can be autocompressed on upload
83+
* [chmod and chown](#chmod-and-chown) - per-volume filesystem-permissions and ownership
8384
* [other flags](#other-flags)
8485
* [database location](#database-location) - in-volume (`.hist/up2k.db`, default) or somewhere else
8586
* [metadata from audio files](#metadata-from-audio-files) - set `-e2t` to index tags on upload
@@ -1649,6 +1650,26 @@ some examples,
16491650
allows (but does not force) gz compression if client uploads to `/inc?pk` or `/inc?gz` or `/inc?gz=4`
16501651
16511652
1653+
## chmod and chown
1654+
1655+
per-volume filesystem-permissions and ownership
1656+
1657+
by default:
1658+
* all folders are chmod 755
1659+
* files are usually chmod 644 (umask-defined)
1660+
* user/group is whatever copyparty is running as
1661+
1662+
this can be configured per-volume:
1663+
* volflag `chmod_f` sets file permissions; default=`644` (usually)
1664+
* volflag `chmod_d` sets directory permissions; default=`755`
1665+
* volflag `uid` sets the owner user-id
1666+
* volflag `gid` sets the owner group-id
1667+
1668+
notes:
1669+
* `gid` can only be set to one of the groups which the copyparty process is a member of
1670+
* `uid` can only be set if copyparty is running as root (i appreciate your faith)
1671+
1672+
16521673
## other flags
16531674
16541675
* `:c,magic` enables filetype detection for nameless uploads, same as `--magic`

copyparty/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,8 @@ def add_upload(ap):
10531053
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
10541054
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
10551055
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
1056+
ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
1057+
ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
10561058
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
10571059
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
10581060
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")

copyparty/authsrv.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ def __init__(self, log_func: Optional["RootLogger"]) -> None:
140140
self.reg: Optional[dict[str, dict[str, Any]]] = None # up2k registry
141141

142142
self.chmod_d = 0o755
143+
self.uid = self.gid = -1
144+
self.chown = False
143145

144146
self.nups: dict[str, list[float]] = {} # num tracker
145147
self.bups: dict[str, list[tuple[float, int]]] = {} # byte tracker list
@@ -302,6 +304,8 @@ def dive(self, path: str, lvs: int) -> Optional[str]:
302304
# no branches yet; make one
303305
sub = os.path.join(path, "0")
304306
bos.mkdir(sub, self.chmod_d)
307+
if self.chown:
308+
os.chown(sub, self.uid, self.gid)
305309
else:
306310
# try newest branch only
307311
sub = os.path.join(path, str(dirs[-1]))
@@ -317,6 +321,8 @@ def dive(self, path: str, lvs: int) -> Optional[str]:
317321
# make a branch
318322
sub = os.path.join(path, str(dirs[-1] + 1))
319323
bos.mkdir(sub, self.chmod_d)
324+
if self.chown:
325+
os.chown(sub, self.uid, self.gid)
320326
ret = self.dive(sub, lvs - 1)
321327
if ret is None:
322328
raise Pebkac(500, "rotation bug")
@@ -2181,7 +2187,7 @@ def _reload(self, verbosity: int = 9) -> None:
21812187
if vf not in vol.flags:
21822188
vol.flags[vf] = getattr(self.args, ga)
21832189

2184-
zs = "forget_ip nrand tail_who u2abort u2ow ups_who zip_who"
2190+
zs = "forget_ip gid nrand tail_who u2abort u2ow uid ups_who zip_who"
21852191
for k in zs.split():
21862192
if k in vol.flags:
21872193
vol.flags[k] = int(vol.flags[k])
@@ -2218,8 +2224,17 @@ def _reload(self, verbosity: int = 9) -> None:
22182224
if (is_d and zi != 0o755) or not is_d:
22192225
free_umask = True
22202226

2227+
vol.flags.pop("chown", None)
2228+
if vol.flags["uid"] != -1 or vol.flags["gid"] != -1:
2229+
vol.flags["chown"] = True
2230+
vol.flags.pop("fperms", None)
2231+
if "chown" in vol.flags or vol.flags.get("chmod_f"):
2232+
vol.flags["fperms"] = True
22212233
if vol.lim:
22222234
vol.lim.chmod_d = vol.flags["chmod_d"]
2235+
vol.lim.chown = "chown" in vol.flags
2236+
vol.lim.uid = vol.flags["uid"]
2237+
vol.lim.gid = vol.flags["gid"]
22232238

22242239
if vol.flags.get("og"):
22252240
self.args.uqe = True

copyparty/bos/bos.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
if True: # pylint: disable=using-constant-test
1010
from typing import Any, Optional
1111

12-
_ = (path,)
13-
__all__ = ["path"]
12+
MKD_755 = {"chmod_d": 0o755}
13+
MKD_700 = {"chmod_d": 0o700}
14+
15+
_ = (path, MKD_755, MKD_700)
16+
__all__ = ["path", "MKD_755", "MKD_700"]
1417

1518
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
1619
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
@@ -20,11 +23,15 @@ def chmod(p: str, mode: int) -> None:
2023
return os.chmod(fsenc(p), mode)
2124

2225

26+
def chown(p: str, uid: int, gid: int) -> None:
27+
return os.chown(fsenc(p), uid, gid)
28+
29+
2330
def listdir(p: str = ".") -> list[str]:
2431
return [fsdec(x) for x in os.listdir(fsenc(p))]
2532

2633

27-
def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool:
34+
def makedirs(name: str, vf: dict[str, Any] = MKD_755, exist_ok: bool = True) -> bool:
2835
# os.makedirs does 777 for all but leaf; this does mode on all
2936
todo = []
3037
bname = fsenc(name)
@@ -37,9 +44,13 @@ def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool:
3744
if not exist_ok:
3845
os.mkdir(bname) # to throw
3946
return False
47+
mode = vf["chmod_d"]
48+
chown = "chown" in vf
4049
for zb in todo[::-1]:
4150
try:
4251
os.mkdir(zb, mode)
52+
if chown:
53+
os.chown(zb, vf["uid"], vf["gid"])
4354
except:
4455
if os.path.isdir(zb):
4556
continue

copyparty/cfg.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ def vf_vmap() -> dict[str, str]:
114114
"unlist",
115115
"u2abort",
116116
"u2ts",
117+
"uid",
118+
"gid",
117119
"ups_who",
118120
"zip_who",
119121
"zipmaxn",
@@ -175,6 +177,8 @@ def vf_cmap() -> dict[str, str]:
175177
"nodupe": "rejects existing files (instead of linking/cloning them)",
176178
"chmod_d=755": "unix-permission for new dirs/folders",
177179
"chmod_f=644": "unix-permission for new files",
180+
"uid=573": "change owner of new files/folders to unix-user 573",
181+
"gid=999": "change owner of new files/folders to unix-group 999",
178182
"sparse": "force use of sparse files, mainly for s3-backed storage",
179183
"nosparse": "deny use of sparse files, mainly for slow storage",
180184
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",

copyparty/ftpd.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
relchk,
3232
runhook,
3333
sanitize_fn,
34+
set_fperms,
3435
vjoin,
3536
wunlink,
3637
)
@@ -262,8 +263,8 @@ def open(self, filename: str, mode: str) -> typing.IO[Any]:
262263
wunlink(self.log, ap, VF_CAREFUL)
263264

264265
ret = open(fsenc(ap), mode, self.args.iobuf)
265-
if w and "chmod_f" in vfs.flags:
266-
os.fchmod(ret.fileno(), vfs.flags["chmod_f"])
266+
if w and "fperms" in vfs.flags:
267+
set_fperms(ret, vfs.flags)
267268

268269
return ret
269270

@@ -297,8 +298,7 @@ def chdir(self, path: str) -> None:
297298

298299
def mkdir(self, path: str) -> None:
299300
ap, vfs, _ = self.rv2a(path, w=True)
300-
chmod = vfs.flags["chmod_d"]
301-
bos.makedirs(ap, chmod) # filezilla expects this
301+
bos.makedirs(ap, vf=vfs.flags) # filezilla expects this
302302

303303
def listdir(self, path: str) -> list[str]:
304304
vpath = join(self.cwd, path)

copyparty/httpcli.py

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
sanitize_vpath,
104104
sendfile_kern,
105105
sendfile_py,
106+
set_fperms,
106107
stat_resource,
107108
ub64dec,
108109
ub64enc,
@@ -2086,7 +2087,7 @@ def dump_to_file(self, is_put: bool) -> tuple[int, str, str, str, int, str, str]
20862087
fdir, fn = os.path.split(fdir)
20872088
rem, _ = vsplit(rem)
20882089

2089-
bos.makedirs(fdir, vfs.flags["chmod_d"])
2090+
bos.makedirs(fdir, vf=vfs.flags)
20902091

20912092
open_ka: dict[str, Any] = {"fun": open}
20922093
open_a = ["wb", self.args.iobuf]
@@ -2144,9 +2145,7 @@ def dump_to_file(self, is_put: bool) -> tuple[int, str, str, str, int, str, str]
21442145
if nameless:
21452146
fn = vfs.flags["put_name2"].format(now=time.time(), cip=self.dip())
21462147

2147-
params = {"suffix": suffix, "fdir": fdir}
2148-
if "chmod_f" in vfs.flags:
2149-
params["chmod"] = vfs.flags["chmod_f"]
2148+
params = {"suffix": suffix, "fdir": fdir, "vf": vfs.flags}
21502149
if self.args.nw:
21512150
params = {}
21522151
fn = os.devnull
@@ -2195,7 +2194,7 @@ def dump_to_file(self, is_put: bool) -> tuple[int, str, str, str, int, str, str]
21952194
if self.args.nw:
21962195
fn = os.devnull
21972196
else:
2198-
bos.makedirs(fdir, vfs.flags["chmod_d"])
2197+
bos.makedirs(fdir, vf=vfs.flags)
21992198
path = os.path.join(fdir, fn)
22002199
if not nameless:
22012200
self.vpath = vjoin(self.vpath, fn)
@@ -2327,7 +2326,7 @@ def dump_to_file(self, is_put: bool) -> tuple[int, str, str, str, int, str, str]
23272326
if self.args.hook_v:
23282327
log_reloc(self.log, hr["reloc"], x, path, vp, fn, vfs, rem)
23292328
fdir, self.vpath, fn, (vfs, rem) = x
2330-
bos.makedirs(fdir, vfs.flags["chmod_d"])
2329+
bos.makedirs(fdir, vf=vfs.flags)
23312330
path2 = os.path.join(fdir, fn)
23322331
atomic_move(self.log, path, path2, vfs.flags)
23332332
path = path2
@@ -2613,7 +2612,7 @@ def handle_post_json(self) -> bool:
26132612
dst = vfs.canonical(rem)
26142613
try:
26152614
if not bos.path.isdir(dst):
2616-
bos.makedirs(dst, vfs.flags["chmod_d"])
2615+
bos.makedirs(dst, vf=vfs.flags)
26172616
except OSError as ex:
26182617
self.log("makedirs failed %r" % (dst,))
26192618
if not bos.path.isdir(dst):
@@ -3060,7 +3059,7 @@ def _mkdir(self, vpath: str, dav: bool = False) -> bool:
30603059
raise Pebkac(405, 'folder "/%s" already exists' % (vpath,))
30613060

30623061
try:
3063-
bos.makedirs(fn, vfs.flags["chmod_d"])
3062+
bos.makedirs(fn, vf=vfs.flags)
30643063
except OSError as ex:
30653064
if ex.errno == errno.EACCES:
30663065
raise Pebkac(500, "the server OS denied write-access")
@@ -3102,8 +3101,8 @@ def handle_new_md(self) -> bool:
31023101

31033102
with open(fsenc(fn), "wb") as f:
31043103
f.write(b"`GRUNNUR`\n")
3105-
if "chmod_f" in vfs.flags:
3106-
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
3104+
if "fperms" in vfs.flags:
3105+
set_fperms(f, vfs.flags)
31073106

31083107
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
31093108
self.redirect(vpath, "?edit")
@@ -3177,7 +3176,7 @@ def handle_plain_upload(
31773176
)
31783177
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
31793178
if not nullwrite:
3180-
bos.makedirs(fdir_base, vfs.flags["chmod_d"])
3179+
bos.makedirs(fdir_base, vf=vfs.flags)
31813180

31823181
rnd, lifetime, xbu, xau = self.upload_flags(vfs)
31833182
zs = self.uparam.get("want") or self.headers.get("accept") or ""
@@ -3210,7 +3209,7 @@ def handle_plain_upload(
32103209
if rnd:
32113210
fname = rand_name(fdir, fname, rnd)
32123211

3213-
open_args = {"fdir": fdir, "suffix": suffix}
3212+
open_args = {"fdir": fdir, "suffix": suffix, "vf": vfs.flags}
32143213

32153214
if "replace" in self.uparam:
32163215
if not self.can_delete:
@@ -3272,11 +3271,8 @@ def handle_plain_upload(
32723271
else:
32733272
open_args["fdir"] = fdir
32743273

3275-
if "chmod_f" in vfs.flags:
3276-
open_args["chmod"] = vfs.flags["chmod_f"]
3277-
32783274
if p_file and not nullwrite:
3279-
bos.makedirs(fdir, vfs.flags["chmod_d"])
3275+
bos.makedirs(fdir, vf=vfs.flags)
32803276

32813277
# reserve destination filename
32823278
f, fname = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
@@ -3380,7 +3376,7 @@ def handle_plain_upload(
33803376
if nullwrite:
33813377
fdir = ap2 = ""
33823378
else:
3383-
bos.makedirs(fdir, vfs.flags["chmod_d"])
3379+
bos.makedirs(fdir, vf=vfs.flags)
33843380
atomic_move(self.log, abspath, ap2, vfs.flags)
33853381
abspath = ap2
33863382
sz = bos.path.getsize(abspath)
@@ -3501,8 +3497,8 @@ def handle_plain_upload(
35013497
ft = "{}:{}".format(self.ip, self.addr[1])
35023498
ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg)
35033499
f.write(ft.encode("utf-8"))
3504-
if "chmod_f" in vfs.flags:
3505-
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
3500+
if "fperms" in vfs.flags:
3501+
set_fperms(f, vfs.flags)
35063502
except Exception as ex:
35073503
suf = "\nfailed to write the upload report: {}".format(ex)
35083504

@@ -3553,7 +3549,7 @@ def handle_text_upload(self) -> bool:
35533549
lim = vfs.get_dbv(rem)[0].lim
35543550
if lim:
35553551
fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker)
3556-
bos.makedirs(fp, vfs.flags["chmod_d"])
3552+
bos.makedirs(fp, vf=vfs.flags)
35573553

35583554
fp = os.path.join(fp, fn)
35593555
rem = "{}/{}".format(rp, fn).strip("/")
@@ -3621,15 +3617,17 @@ def handle_text_upload(self) -> bool:
36213617
zs = ub64enc(zb).decode("ascii")[:24].lower()
36223618
dp = "%s/md/%s/%s/%s" % (dbv.histpath, zs[:2], zs[2:4], zs)
36233619
self.log("moving old version to %s/%s" % (dp, mfile2))
3624-
if bos.makedirs(dp, vfs.flags["chmod_d"]):
3620+
if bos.makedirs(dp, vf=vfs.flags):
36253621
with open(os.path.join(dp, "dir.txt"), "wb") as f:
36263622
f.write(afsenc(vrd))
3627-
if "chmod_f" in vfs.flags:
3628-
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
3623+
if "fperms" in vfs.flags:
3624+
set_fperms(f, vfs.flags)
36293625
elif hist_cfg == "s":
36303626
dp = os.path.join(mdir, ".hist")
36313627
try:
36323628
bos.mkdir(dp, vfs.flags["chmod_d"])
3629+
if "chown" in vfs.flags:
3630+
bos.chown(dp, vfs.flags["uid"], vfs.flags["gid"])
36333631
hidedir(dp)
36343632
except:
36353633
pass
@@ -3668,8 +3666,8 @@ def handle_text_upload(self) -> bool:
36683666
wunlink(self.log, fp, vfs.flags)
36693667

36703668
with open(fsenc(fp), "wb", self.args.iobuf) as f:
3671-
if "chmod_f" in vfs.flags:
3672-
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
3669+
if "fperms" in vfs.flags:
3670+
set_fperms(f, vfs.flags)
36733671
sz, sha512, _ = hashcopy(p_data, f, None, 0, self.args.s_wr_slp)
36743672

36753673
if lim:

copyparty/smbd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ def _rename(self, vp1: str, vp2: str) -> None:
320320

321321
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
322322
try:
323-
bos.makedirs(ap2, vfs2.flags["chmod_d"])
323+
bos.makedirs(ap2, vf=vfs2.flags)
324324
except:
325325
pass
326326

copyparty/tftpd.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def __init__(self, **attr):
4545
exclude_dotfiles,
4646
min_ex,
4747
runhook,
48+
set_fperms,
4849
undot,
4950
vjoin,
5051
vsplit,
@@ -388,8 +389,8 @@ def _open(self, vpath: str, mode: str, *a: Any, **ka: Any) -> Any:
388389
a = (self.args.iobuf,)
389390

390391
ret = open(ap, mode, *a, **ka)
391-
if wr and "chmod_f" in vfs.flags:
392-
os.fchmod(ret.fileno(), vfs.flags["chmod_f"])
392+
if wr and "fperms" in vfs.flags:
393+
set_fperms(ret, vfs.flags)
393394

394395
return ret
395396

@@ -398,7 +399,9 @@ def _mkdir(self, vpath: str, *a) -> None:
398399
if "*" not in vfs.axs.uwrite:
399400
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
400401

401-
return bos.mkdir(ap, vfs.flags["chmod_d"])
402+
bos.mkdir(ap, vfs.flags["chmod_d"])
403+
if "chown" in vfs.flags:
404+
bos.chown(ap, vfs.flags["uid"], vfs.flags["gid"])
402405

403406
def _unlink(self, vpath: str) -> None:
404407
# return bos.unlink(self._v2a("stat", vpath, *a)[1])

0 commit comments

Comments
 (0)