From d570f04d260df226c3a244fa9bc65768f0a228ea Mon Sep 17 00:00:00 2001 From: mengshon1-boop <1362668392@qq.com> Date: Sat, 25 Apr 2026 18:28:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=A7=92=E8=89=B2=E6=9D=83=E9=99=90=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=8F=8A=E5=88=86=E4=BA=AB=E7=AE=A1=E7=90=86=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增角色权限系统,支持admin/editor/guest三种角色 添加权限管理页面,展示用户权限、角色及分享信息 扩展分享数据库表结构,增加公开状态、访问次数限制字段 实现角色权限自动应用到用户的功能 --- copyparty/authsrv.py | 93 +++++++++++++++++++++- copyparty/httpcli.py | 108 +++++++++++++++++++++++++- copyparty/svchub.py | 14 +++- copyparty/web/perms.html | 163 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 369 insertions(+), 9 deletions(-) create mode 100644 copyparty/web/perms.html diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 7dde17f4..c77af8bd 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -74,6 +74,12 @@ if PY2: LEELOO_DALLAS = "leeloo_dallas" + +ROLE_PERMISSIONS = { + "admin": {"r", "w", "m", "d", "g", "G", "h", "a", "."}, + "editor": {"r", "w", "m", "g", "G", "h"}, + "guest": {"r", "g"}, +} ## ## you might be curious what Leeloo Dallas is doing here, so let me explain: ## @@ -432,6 +438,10 @@ class VFS(object): self.shr_files: set[str] = set() # filenames to include from shr_src self.shr_owner: str = "" # uname self.shr_all_aps: list[tuple[str, list[VFS]]] = [] + self.shr_is_public: bool = False + self.shr_max_visits: int = 0 + self.shr_visit_count: int = 0 + self.shr_key: str = "" self.aread: dict[str, list[str]] = {} self.awrite: dict[str, list[str]] = {} self.amove: dict[str, list[str]] = {} @@ -1097,6 +1107,7 @@ class AuthSrv(object): self.sesa: dict[str, str] = {} # session->uname self.defpw: dict[str, str] = {} self.grps: dict[str, list[str]] = {} + self.roles: dict[str, str] = {} self.re_pwd: Optional[re.Pattern] = None self.cfg_files_loaded: list[str] = [] self.badcfg1 = False @@ -1116,6 +1127,49 @@ class AuthSrv(object): if self.log_func: self.log_func("auth", msg, c) + def _apply_role_permissions(self, vfs: "VFS", unames: list[str]) -> None: + """ + Apply role-based permissions to users. + Roles: admin, editor, guest + """ + perm_map = { + "r": "uread", + "w": "uwrite", + "m": "umove", + "d": "udel", + "g": "uget", + "G": "upget", + "h": "uhtml", + "a": "uadmin", + ".": "udot", + } + + for uname in unames: + if uname == "*": + continue + + role = self.roles.get(uname) + if not role: + continue + + role_perms = ROLE_PERMISSIONS.get(role) + if not role_perms: + continue + + if self.args.vc: + self._l("roles", 5, "applying role permissions for user [%s] with role [%s]" % (uname, role)) + + for vol in vfs.all_vols.values(): + if vol.vpath.startswith(self.args.shr1) if self.args.shr else False: + continue + + for perm_char in role_perms: + perm_attr = perm_map.get(perm_char) + if perm_attr: + perm_set = getattr(vol.axs, perm_attr) + if uname not in perm_set: + perm_set.add(uname) + def laggy_iter(self, iterable: Iterable[Any]) -> Generator[Any, None, None]: """returns [value,isFinalValue]""" it = iter(iterable) @@ -1401,6 +1455,7 @@ class AuthSrv(object): catg = "[global]" cata = "[accounts]" catgrp = "[groups]" + catroles = "[roles]" catx = "accs:" catf = "flags:" ap: Optional[str] = None @@ -1436,6 +1491,8 @@ class AuthSrv(object): self._l(ln, 5, "begin user-accounts section") elif ln == catgrp: self._l(ln, 5, "begin user-groups section") + elif ln == catroles: + self._l(ln, 5, "begin user-roles section") elif ln.startswith("[/"): vp = ln[1:-1].strip("/") self._l(ln, 2, "define volume at URL [/{}]".format(vp)) @@ -1496,6 +1553,25 @@ class AuthSrv(object): raise Exception(t + SBADCFG) continue + if cat == catroles: + try: + rn, zs1 = [zs.strip() for zs in ln.split(":", 1)] + uns = [zs.strip() for zs in zs1.split(",")] + rn = rn.lower() + valid_roles = ["admin", "editor", "guest"] + if rn not in valid_roles: + t = 'invalid role name "%s", valid roles are: %s' + raise Exception(t % (rn, ", ".join(valid_roles))) + t = "role [%s] = " % (rn,) + t += ", ".join("user [%s]" % (x,) for x in uns) + self._l(ln, 5, t) + for un in uns: + self.roles[un] = rn + except: + t = 'lines inside the [roles] section must be "rolename: user1, user2, user..."' + raise Exception(t + SBADCFG) + continue + if vp is not None and ap is None: if npass != 2: continue @@ -1960,10 +2036,17 @@ class AuthSrv(object): cur2 = db.cursor() now = time.time() for row in cur.execute("select * from sh"): - s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row + s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row[:8] + s_mv = row[8] if len(row) > 8 else 0 + s_vc = row[9] if len(row) > 9 else 0 + s_pub = row[10] if len(row) > 10 else 0 + if s_t1 and s_t1 < now: continue + if s_mv and s_vc >= s_mv: + continue + if self.args.shr_v: t = "loading %s share %r by %r => %r" self.log(t % (s_pr, s_k, s_un, s_vp)) @@ -1993,6 +2076,12 @@ class AuthSrv(object): # still has the privs they granted, so nullmap it vp = "%s/%s" % (shr, s_k) shv.nodes[s_k] = VFS(self.log_func, "", vp, vp, s_axs, shv.flags.copy()) + shn = shv.nodes[s_k] + shn.shr_key = s_k + shn.shr_is_public = bool(s_pub) + shn.shr_max_visits = s_mv + shn.shr_visit_count = s_vc + shn.shr_owner = s_un vfs.nodes[shr] = vfs.all_vols[shr] = shv for vol in shv.nodes.values(): @@ -2005,6 +2094,8 @@ class AuthSrv(object): zss.discard("*") unames = ["*"] + list(sorted(zss)) + self._apply_role_permissions(vfs, unames) + for perm in "read write move del get pget html admin dot".split(): axs_key = "u" + perm for vp, vol in vfs.all_vols.items(): diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index d8df0715..073fe5c3 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -32,7 +32,7 @@ except: from .__init__ import ANYWIN, RES, RESM, TYPE_CHECKING, EnvParams, unicode from .__version__ import S_VERSION -from .authsrv import LEELOO_DALLAS, VFS # typechk +from .authsrv import LEELOO_DALLAS, ROLE_PERMISSIONS, VFS # typechk from .bos import bos from .qrkode import QrCode, qr2svg, qrgen from .star import StreamTar @@ -1505,6 +1505,9 @@ class HttpCli(object): if "shares" in self.uparam: return self.tx_shares() + if "perms" in self.uparam: + return self.tx_perms() + if "dls" in self.uparam: return self.tx_dls() @@ -6336,6 +6339,95 @@ class HttpCli(object): self.reply(html.encode("utf-8"), status=200) return True + def tx_perms(self) -> bool: + if self.uname == "*": + self.loud_reply("you're not logged in") + return True + + user_role = self.asrv.roles.get(self.uname, "") + is_admin = user_role == "admin" + + volumes = [] + vfs = self.asrv.vfs + for vp, vol in vfs.all_vols.items(): + if vp.startswith(self.args.shr.strip("/") + "/") if self.args.shr else False: + continue + + uaxs = vol.uaxs.get(self.uname, (False, False, False, False, False, False, False, False, False)) + + volumes.append({ + "vpath": vp, + "read": uaxs[0], + "write": uaxs[1], + "move": uaxs[2], + "delete": uaxs[3], + "get": uaxs[4], + "admin": uaxs[7], + "shared": False, + }) + + for vol in volumes: + for share_vol in vfs.all_nodes.values(): + if hasattr(share_vol, "shr_src") and share_vol.shr_src: + svn, _ = share_vol.shr_src + if svn.vpath == vol["vpath"]: + vol["shared"] = True + break + + perms = self.asrv.get_perms("/", self.uname) + + users = [] + if is_admin: + acct = self.asrv.acct + for uname in sorted(acct.keys()): + if uname == "*": + continue + role = self.asrv.roles.get(uname, "") + user_perms = self.asrv.get_perms("/", uname) + users.append({ + "name": uname, + "role": role, + "perms": user_perms, + }) + + shares = [] + if self.args.shr: + idx = self.conn.get_u2idx() + if idx and hasattr(idx, "p_end"): + cur = idx.get_shr() + if cur: + rows = cur.execute("select * from sh").fetchall() + now = int(time.time()) + for row in rows: + s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1, s_mv, s_vc, s_pub = row + if is_admin or s_un == self.uname: + shares.append({ + "k": s_k, + "vp": s_vp, + "pr": s_pr, + "un": s_un, + "pub": bool(s_pub), + "t1": s_t1, + "vc": s_vc, + "mv": s_mv, + }) + + html = self.j2s( + "perms", + this=self, + uname=self.uname, + role=user_role, + perms=perms, + is_admin=is_admin, + volumes=volumes, + users=users, + shares=shares, + shr=self.args.shr, + now=int(time.time()), + ) + self.reply(html.encode("utf-8"), status=200) + return True + def handle_eshare(self) -> bool: idx = self.conn.get_u2idx() if not idx or not hasattr(idx, "p_end"): @@ -6458,7 +6550,11 @@ class HttpCli(object): raise Pebkac(400, "you dont have all the perms you tried to grant") zs = vfs.flags["shr_who"] - if zs == "auth" and self.uname != "*": + user_role = self.asrv.roles.get(self.uname) + + if user_role in ["admin", "editor"]: + pass + elif zs == "auth" and self.uname != "*": pass elif zs == "a" and self.uname in vfs.axs.uadmin: pass @@ -6482,8 +6578,12 @@ class HttpCli(object): exp = now + exp * 60 if exp else 0 pr = "".join(zc for zc, zb in zip("rwmdg.", s_axsd) if zb) - q = "insert into sh values (?,?,?,?,?,?,?,?)" - cur.execute(q, (skey, pw, vp, pr, len(fns), self.uname, now, exp)) + max_visits = int(req.get("max_visits") or 0) + visit_count = 0 + is_public = 1 if req.get("is_public") else 0 + + q = "insert into sh values (?,?,?,?,?,?,?,?,?,?,?)" + cur.execute(q, (skey, pw, vp, pr, len(fns), self.uname, now, exp, max_visits, visit_count, is_public)) q = "insert into sf values (?,?)" for fn in fns: diff --git a/copyparty/svchub.py b/copyparty/svchub.py index fc0a9db9..305bf350 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -109,7 +109,7 @@ else: VER_IDP_DB = 1 VER_SESSION_DB = 1 -VER_SHARES_DB = 2 +VER_SHARES_DB = 3 class SvcHub(object): @@ -757,15 +757,14 @@ class SvcHub(object): sch1 = [ r"create table kv (k text, v int)", - r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)", - # sharekey, password, src, perms, numFiles, owner, created, expires + r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int, mv int, vc int, pub int)", ] sch2 = [ r"create table sf (k text, vp text)", r"create index sf_k on sf(k)", r"create index sh_k on sh(k)", r"create index sh_t1 on sh(t1)", - r"insert into kv values ('sver', 2)", + r"insert into kv values ('sver', 3)", ] assert db # type: ignore # !rm @@ -782,6 +781,13 @@ class SvcHub(object): cur.execute("update sh set st = 0") self.log("root", "shares-db schema upgrade ok") + if sver == 2: + cur.execute("alter table sh add column mv int") + cur.execute("alter table sh add column vc int") + cur.execute("alter table sh add column pub int") + cur.execute("update sh set mv = 0, vc = 0, pub = 0") + self.log("root", "shares-db schema upgrade to v3 ok") + if sver < VER_SHARES_DB: cur.execute("delete from kv where k='sver'") cur.execute("insert into kv values('sver',?)", (VER_SHARES_DB,)) diff --git a/copyparty/web/perms.html b/copyparty/web/perms.html new file mode 100644 index 00000000..12656079 --- /dev/null +++ b/copyparty/web/perms.html @@ -0,0 +1,163 @@ + + + + + + {{ s_doctitle }} + + + + + +{{ html_head }} + + + +
+ refresh + my shares + control-panel + browse + +

User Role and Permissions Management

+ +
+

Your Profile

+ + + + + + + + + + + + + +
Username{{ uname }}
Role{{ role or "default" }}
Permissions{{ perms }}
+
+ +

Role Descriptions

+ + + + + + + + + + + + + + + + + + + + + + + + + +
RolePermissionsDescription
adminr, w, m, d, g, G, h, a, .Full access: can read, write, move, delete, and manage all files and permissions
editorr, w, m, g, G, hCan read, write, and move files, but cannot delete or manage permissions
guestr, gCan only view public files, cannot edit or share
+ +

Your Volumes

+ + + + + + + + + + + + + + + {%- for vol in volumes %} + + + + + + + + + + + {%- endfor %} + +
VolumeReadWriteMoveDeleteGetAdminShared
/{{ vol.vpath|e }}{{ '✓' if vol.read else '✗' }}{{ '✓' if vol.write else '✗' }}{{ '✓' if vol.move else '✗' }}{{ '✓' if vol.delete else '✗' }}{{ '✓' if vol.get else '✗' }}{{ '✓' if vol.admin else '✗' }}{{ '✓' if vol.shared else '✗' }}
+ + {%- if is_admin %} +

All Users (Admin Only)

+ + + + + + + + + + {%- for user in users %} + + + + + + {%- endfor %} + +
UsernameRolePermissions
{{ user.name|e }}{{ user.role or "default" }}{{ user.perms }}
+ {%- endif %} + + {%- if shares %} +

Active Shares

+ + + + + + + + + + + + + + {%- for share in shares %} + + + + + + + + + + {%- endfor %} + +
Share KeySourcePermissionsOwnerPublicExpiresVisits
{{ share.k }}/{{ share.vp|e }}{{ share.pr }}{{ share.un|e }}{{ '✓' if share.pub else '✗' }}{{ "never" if not share.t1 else "expired" if share.t1 < now else "in " ~ ((share.t1 - now) // 60) ~ " min" }}{{ share.vc }}{% if share.mv %} / {{ share.mv }}{% endif %}
+ {%- endif %} +
+ + + + + \ No newline at end of file