mirror of
https://github.com/9001/copyparty.git
synced 2026-06-22 14:02:53 -06:00
feat(权限管理): 添加角色权限系统及分享管理页面
新增角色权限系统,支持admin/editor/guest三种角色 添加权限管理页面,展示用户权限、角色及分享信息 扩展分享数据库表结构,增加公开状态、访问次数限制字段 实现角色权限自动应用到用户的功能
This commit is contained in:
parent
6e25d648a9
commit
d570f04d26
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,))
|
||||
|
|
|
|||
163
copyparty/web/perms.html
Normal file
163
copyparty/web/perms.html
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" id="ht_perms">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ s_doctitle }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/w/ui.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<a href="{{ r }}/?perms">refresh</a>
|
||||
<a href="{{ r }}/?shares">my shares</a>
|
||||
<a href="{{ r }}/?h">control-panel</a>
|
||||
<a href="{{ r }}/">browse</a>
|
||||
|
||||
<h2>User Role and Permissions Management</h2>
|
||||
|
||||
<div id="user-info">
|
||||
<h3>Your Profile</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<td>{{ uname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Role</th>
|
||||
<td>{{ role or "default" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Permissions</th>
|
||||
<td>{{ perms }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Role Descriptions</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role</th>
|
||||
<th>Permissions</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>admin</td>
|
||||
<td>r, w, m, d, g, G, h, a, .</td>
|
||||
<td>Full access: can read, write, move, delete, and manage all files and permissions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>editor</td>
|
||||
<td>r, w, m, g, G, h</td>
|
||||
<td>Can read, write, and move files, but cannot delete or manage permissions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>guest</td>
|
||||
<td>r, g</td>
|
||||
<td>Can only view public files, cannot edit or share</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Your Volumes</h3>
|
||||
<table id="volumes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Volume</th>
|
||||
<th>Read</th>
|
||||
<th>Write</th>
|
||||
<th>Move</th>
|
||||
<th>Delete</th>
|
||||
<th>Get</th>
|
||||
<th>Admin</th>
|
||||
<th>Shared</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for vol in volumes %}
|
||||
<tr>
|
||||
<td><a href="{{ r }}/{{ vol.vpath|e }}">/{{ vol.vpath|e }}</a></td>
|
||||
<td class="{{ 'yes' if vol.read else 'no' }}">{{ '✓' if vol.read else '✗' }}</td>
|
||||
<td class="{{ 'yes' if vol.write else 'no' }}">{{ '✓' if vol.write else '✗' }}</td>
|
||||
<td class="{{ 'yes' if vol.move else 'no' }}">{{ '✓' if vol.move else '✗' }}</td>
|
||||
<td class="{{ 'yes' if vol.delete else 'no' }}">{{ '✓' if vol.delete else '✗' }}</td>
|
||||
<td class="{{ 'yes' if vol.get else 'no' }}">{{ '✓' if vol.get else '✗' }}</td>
|
||||
<td class="{{ 'yes' if vol.admin else 'no' }}">{{ '✓' if vol.admin else '✗' }}</td>
|
||||
<td class="{{ 'yes' if vol.shared else 'no' }}">{{ '✓' if vol.shared else '✗' }}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{%- if is_admin %}
|
||||
<h3>All Users (Admin Only)</h3>
|
||||
<table id="all-users">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th>Permissions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.name|e }}</td>
|
||||
<td>{{ user.role or "default" }}</td>
|
||||
<td>{{ user.perms }}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endif %}
|
||||
|
||||
{%- if shares %}
|
||||
<h3>Active Shares</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Share Key</th>
|
||||
<th>Source</th>
|
||||
<th>Permissions</th>
|
||||
<th>Owner</th>
|
||||
<th>Public</th>
|
||||
<th>Expires</th>
|
||||
<th>Visits</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for share in shares %}
|
||||
<tr>
|
||||
<td><a href="{{ r }}{{ shr }}{{ share.k }}/">{{ share.k }}</a></td>
|
||||
<td><a href="{{ r }}/{{ share.vp|e }}">/{{ share.vp|e }}</a></td>
|
||||
<td>{{ share.pr }}</td>
|
||||
<td>{{ share.un|e }}</td>
|
||||
<td class="{{ 'yes' if share.pub else 'no' }}">{{ '✓' if share.pub else '✗' }}</td>
|
||||
<td>{{ "never" if not share.t1 else "expired" if share.t1 < now else "in " ~ ((share.t1 - now) // 60) ~ " min" }}</td>
|
||||
<td>{{ share.vc }}{% if share.mv %} / {{ share.mv }}{% endif %}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var SR="{{ r }}",
|
||||
lang="{{ lang }}",
|
||||
dfavico="{{ favico }}";
|
||||
|
||||
var STG = window.localStorage;
|
||||
document.documentElement.className = (STG && STG.cpp_thm) || "{{ this.args.theme }}";
|
||||
</script>
|
||||
<script src="{{ r }}/.cpr/w/util.js?_={{ ts }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue