Compare commits
6 Commits
fd9ef322c7
...
835a2ff3f7
| Author | SHA1 | Date | |
|---|---|---|---|
| 835a2ff3f7 | |||
| 8cec17c042 | |||
| 1b21f46336 | |||
| f8c8a4ce4d | |||
| 0b456486db | |||
| c53c16d54c |
42
rbac/init.py
42
rbac/init.py
@ -5,22 +5,28 @@ from .orgs import (
|
|||||||
get_platform_providers
|
get_platform_providers
|
||||||
)
|
)
|
||||||
from .userperm import UserPermissions
|
from .userperm import UserPermissions
|
||||||
|
from .user_stats import get_user_stats
|
||||||
|
from .rbac_tools import (
|
||||||
|
query_path_roles,
|
||||||
|
scan_unauth_files
|
||||||
|
)
|
||||||
from rbac.check_perm import (
|
from rbac.check_perm import (
|
||||||
objcheckperm,
|
objcheckperm,
|
||||||
get_org_users,
|
get_org_users,
|
||||||
sor_get_org_users,
|
sor_get_org_users,
|
||||||
checkUserPassword,
|
checkUserPassword,
|
||||||
register_user,
|
register_user,
|
||||||
register_auth_method,
|
register_auth_method,
|
||||||
create_org,
|
create_org,
|
||||||
create_user
|
create_user
|
||||||
)
|
)
|
||||||
from rbac.set_role_perms import (
|
from rbac.set_role_perms import (
|
||||||
sor_add_user_roles,
|
sor_add_user_roles,
|
||||||
set_role_perm,
|
set_role_perm,
|
||||||
set_role_perms
|
set_role_perms
|
||||||
)
|
)
|
||||||
from appPublic.log import debug
|
from appPublic.log import debug
|
||||||
|
from ahserver.cache_sync import get_cache_sync
|
||||||
|
|
||||||
async def get_owner_orgid(*args, **kw):
|
async def get_owner_orgid(*args, **kw):
|
||||||
return '0'
|
return '0'
|
||||||
@ -59,6 +65,27 @@ def _bind_rbac_events(dbpools, dbname, up):
|
|||||||
debug(f'RBAC event bound: {event_name}')
|
debug(f'RBAC event bound: {event_name}')
|
||||||
|
|
||||||
|
|
||||||
|
async def start_cache_sync():
|
||||||
|
"""Start cache_sync and register RBAC reload callbacks."""
|
||||||
|
env = ServerEnv()
|
||||||
|
cache_sync = get_cache_sync()
|
||||||
|
|
||||||
|
# Get Redis URL from session config
|
||||||
|
try:
|
||||||
|
redis_url = env.conf.website.session_redis.url
|
||||||
|
except AttributeError:
|
||||||
|
redis_url = "redis://127.0.0.1:6379"
|
||||||
|
|
||||||
|
await cache_sync.start(redis_url)
|
||||||
|
debug(f'RBAC cache_sync started with Redis URL: {redis_url}')
|
||||||
|
|
||||||
|
# Register callbacks for cache invalidation messages from other processes
|
||||||
|
up = env.userpermissions
|
||||||
|
cache_sync.register('rbac:rp', up.invalidate_rp_cache)
|
||||||
|
cache_sync.register('rbac:ur:all', up.invalidate_all_user_caches)
|
||||||
|
# Note: rbac:ur:{userid} callbacks are handled by the invalidate_user_cache method itself
|
||||||
|
|
||||||
|
|
||||||
def load_rbac():
|
def load_rbac():
|
||||||
AuthAPI.checkUserPermission = objcheckperm
|
AuthAPI.checkUserPermission = objcheckperm
|
||||||
env = ServerEnv()
|
env = ServerEnv()
|
||||||
@ -76,6 +103,9 @@ def load_rbac():
|
|||||||
env.sor_get_org_users = sor_get_org_users
|
env.sor_get_org_users = sor_get_org_users
|
||||||
env.get_owner_orgid = get_owner_orgid
|
env.get_owner_orgid = get_owner_orgid
|
||||||
env.sor_add_user_roles = sor_add_user_roles
|
env.sor_add_user_roles = sor_add_user_roles
|
||||||
|
env.get_user_stats = get_user_stats
|
||||||
|
env.query_path_roles = query_path_roles
|
||||||
|
env.scan_unauth_files = scan_unauth_files
|
||||||
# Cache invalidation methods for use after role/permission changes
|
# Cache invalidation methods for use after role/permission changes
|
||||||
env.invalidate_user_perm_cache = env.userpermissions.invalidate_user_cache
|
env.invalidate_user_perm_cache = env.userpermissions.invalidate_user_cache
|
||||||
env.invalidate_all_perm_caches = env.userpermissions.invalidate_all_user_caches
|
env.invalidate_all_perm_caches = env.userpermissions.invalidate_all_user_caches
|
||||||
|
|||||||
228
rbac/rbac_tools.py
Normal file
228
rbac/rbac_tools.py
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
"""
|
||||||
|
RBAC 工具函数 — 权限查询与文件扫描。
|
||||||
|
|
||||||
|
提供两个功能:
|
||||||
|
1. 查询指定路径拥有权限的所有角色
|
||||||
|
2. 扫描 wwwroot 下符号链接目录中未授权的文件
|
||||||
|
|
||||||
|
.dspy 文件应调用此模块的函数(通过 request._run_ns),自身不做 import。
|
||||||
|
每个函数返回 (title, message, is_error) 三元组,.dspy 据此返回 UiMessage 或 UiError。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
async def query_path_roles(sor, path):
|
||||||
|
"""
|
||||||
|
查询指定路径拥有权限的角色列表。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(title, message, is_error)
|
||||||
|
"""
|
||||||
|
if not path:
|
||||||
|
return ('错误', '请输入路径', True)
|
||||||
|
if not path.startswith('/'):
|
||||||
|
path = '/' + path
|
||||||
|
|
||||||
|
perm_recs = await sor.sqlExe(
|
||||||
|
"SELECT id, path FROM permission WHERE path=${path}$",
|
||||||
|
{'path': path}
|
||||||
|
)
|
||||||
|
if not perm_recs:
|
||||||
|
like_path = path.rstrip('/') + '/%'
|
||||||
|
like_recs = await sor.sqlExe(
|
||||||
|
"SELECT path FROM permission WHERE path LIKE ${lp}$",
|
||||||
|
{'lp': like_path}
|
||||||
|
)
|
||||||
|
msg = f"路径 '{path}' 未在 permission 表中注册。"
|
||||||
|
if like_recs:
|
||||||
|
sub = '<br>'.join([f' {r.path}' for r in like_recs[:10]])
|
||||||
|
if len(like_recs) > 10:
|
||||||
|
sub += f'<br>... 共 {len(like_recs)} 条'
|
||||||
|
msg += f'<br><br>模糊匹配到 {len(like_recs)} 个子路径:<br>{sub}'
|
||||||
|
return ('未找到', msg, True)
|
||||||
|
|
||||||
|
perm = perm_recs[0]
|
||||||
|
permid = perm.id
|
||||||
|
|
||||||
|
rp_recs = await sor.sqlExe(
|
||||||
|
"SELECT roleid FROM rolepermission WHERE permid=${permid}$",
|
||||||
|
{'permid': permid}
|
||||||
|
)
|
||||||
|
if not rp_recs:
|
||||||
|
msg = f"路径: <b>{path}</b> (perm_id: {permid})<br><br>无任何角色拥有此路径权限。"
|
||||||
|
return ('查询结果', msg, False)
|
||||||
|
|
||||||
|
role_ids = [r.roleid for r in rp_recs]
|
||||||
|
special = [rid for rid in role_ids if rid in ('any', 'anonymous', 'logined')]
|
||||||
|
normal = [rid for rid in role_ids if rid not in ('any', 'anonymous', 'logined')]
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for sp in special:
|
||||||
|
rows.append(f"<tr><td>{sp}</td><td>*</td><td>{sp}</td></tr>")
|
||||||
|
|
||||||
|
if normal:
|
||||||
|
placeholders = ','.join([f'${i}$' for i in range(len(normal))])
|
||||||
|
ns = {f'_{i}': rid for i, rid in enumerate(normal)}
|
||||||
|
role_recs = await sor.sqlExe(
|
||||||
|
f"SELECT id, orgtypeid, name FROM role WHERE id IN ({placeholders})",
|
||||||
|
ns
|
||||||
|
)
|
||||||
|
for r in role_recs:
|
||||||
|
rows.append(
|
||||||
|
f"<tr><td>{r.id}</td>"
|
||||||
|
f"<td>{getattr(r, 'orgtypeid', '*')}</td>"
|
||||||
|
f"<td>{getattr(r, 'name', r.id)}</td></tr>"
|
||||||
|
)
|
||||||
|
|
||||||
|
table = (
|
||||||
|
"<table style='border-collapse:collapse;width:100%;'>"
|
||||||
|
"<tr style='background:#334155;color:#fff;'>"
|
||||||
|
"<th style='padding:6px 12px;text-align:left;border:1px solid #475569;'>角色ID</th>"
|
||||||
|
"<th style='padding:6px 12px;text-align:left;border:1px solid #475569;'>orgtypeid</th>"
|
||||||
|
"<th style='padding:6px 12px;text-align:left;border:1px solid #475569;'>名称</th>"
|
||||||
|
"</tr>"
|
||||||
|
+ ''.join(rows) +
|
||||||
|
"</table>"
|
||||||
|
)
|
||||||
|
|
||||||
|
html = (
|
||||||
|
f"<p>路径: <b>{path}</b> (perm_id: {permid})</p>"
|
||||||
|
f"<p>共 {len(rp_recs)} 个角色有权限:</p>"
|
||||||
|
f"{table}"
|
||||||
|
)
|
||||||
|
return ('查询结果', html, False)
|
||||||
|
|
||||||
|
|
||||||
|
def find_symlink_dirs(root):
|
||||||
|
"""找出指定目录下直接子目录中是符号链接的目录。"""
|
||||||
|
symlinks = []
|
||||||
|
for entry in sorted(os.listdir(root)):
|
||||||
|
full = os.path.join(root, entry)
|
||||||
|
if os.path.islink(full) and os.path.isdir(full):
|
||||||
|
target = os.readlink(full)
|
||||||
|
symlinks.append((entry, full, target))
|
||||||
|
return symlinks
|
||||||
|
|
||||||
|
|
||||||
|
def walk_symlink_dirs(root, symlinks):
|
||||||
|
"""
|
||||||
|
遍历所有符号链接目录下的文件。
|
||||||
|
Returns: list of (relative_path, absolute_path)
|
||||||
|
"""
|
||||||
|
real_root = os.path.realpath(root)
|
||||||
|
visited_real = set()
|
||||||
|
files = []
|
||||||
|
for name, link_path, target in symlinks:
|
||||||
|
for r, dirs, filenames in os.walk(link_path, followlinks=True):
|
||||||
|
real_r = os.path.realpath(r)
|
||||||
|
if real_r in visited_real:
|
||||||
|
dirs.clear()
|
||||||
|
continue
|
||||||
|
visited_real.add(real_r)
|
||||||
|
dirs[:] = [d for d in dirs if os.path.realpath(os.path.join(r, d)) != real_root]
|
||||||
|
for fname in sorted(filenames):
|
||||||
|
abs_path = os.path.join(r, fname)
|
||||||
|
rel = '/' + os.path.relpath(abs_path, root)
|
||||||
|
files.append((rel, abs_path))
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_wwwroot(wwwroot_param, file_path):
|
||||||
|
"""自动定位 wwwroot 目录。"""
|
||||||
|
if wwwroot_param:
|
||||||
|
wwwroot_param = os.path.abspath(wwwroot_param)
|
||||||
|
if os.path.isdir(wwwroot_param):
|
||||||
|
return wwwroot_param, None
|
||||||
|
return None, f"wwwroot 目录不存在: {wwwroot_param}"
|
||||||
|
|
||||||
|
sage_root = os.environ.get('SAGE_ROOT')
|
||||||
|
if sage_root:
|
||||||
|
wr = os.path.join(sage_root, 'wwwroot')
|
||||||
|
if os.path.isdir(wr):
|
||||||
|
return wr, None
|
||||||
|
return None, f"SAGE_ROOT 指向的 wwwroot 不存在: {wr}"
|
||||||
|
|
||||||
|
if '/wwwroot/' in file_path:
|
||||||
|
idx = file_path.index('/wwwroot/')
|
||||||
|
wr = file_path[:idx + len('/wwwroot')]
|
||||||
|
if os.path.isdir(wr):
|
||||||
|
return wr, None
|
||||||
|
|
||||||
|
candidate = file_path
|
||||||
|
for _ in range(5):
|
||||||
|
candidate = os.path.dirname(candidate)
|
||||||
|
if not candidate or candidate == '/':
|
||||||
|
break
|
||||||
|
wr = os.path.join(candidate, 'wwwroot')
|
||||||
|
if os.path.isdir(wr):
|
||||||
|
return wr, None
|
||||||
|
|
||||||
|
return None, '无法自动定位 wwwroot 目录,请传入 wwwroot 参数或设置 SAGE_ROOT 环境变量。'
|
||||||
|
|
||||||
|
|
||||||
|
async def scan_unauth_files(sor, wwwroot_param, file_path):
|
||||||
|
"""
|
||||||
|
扫描 wwwroot 下符号链接目录中未授权的文件。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(title, message, is_error)
|
||||||
|
"""
|
||||||
|
wwwroot, err = resolve_wwwroot(wwwroot_param, file_path)
|
||||||
|
if err:
|
||||||
|
return ('错误', err, True)
|
||||||
|
|
||||||
|
symlinks = find_symlink_dirs(wwwroot)
|
||||||
|
if not symlinks:
|
||||||
|
return ('扫描结果', f"在 {wwwroot} 中未发现任何符号链接目录。", False)
|
||||||
|
|
||||||
|
all_files = walk_symlink_dirs(wwwroot, symlinks)
|
||||||
|
symlink_info = '<br>'.join([
|
||||||
|
f" {name}/ → {target}" for name, _, target in symlinks
|
||||||
|
])
|
||||||
|
|
||||||
|
perm_recs = await sor.sqlExe("SELECT id, path FROM permission", {})
|
||||||
|
path_to_permid = {r.path: r.id for r in perm_recs}
|
||||||
|
|
||||||
|
rp_recs = await sor.sqlExe("SELECT DISTINCT permid FROM rolepermission", {})
|
||||||
|
perms_with_roles = {r.permid for r in rp_recs}
|
||||||
|
|
||||||
|
unauth = []
|
||||||
|
authed = 0
|
||||||
|
for rel_path, abs_path in all_files:
|
||||||
|
permid = path_to_permid.get(rel_path)
|
||||||
|
if permid is None:
|
||||||
|
unauth.append((rel_path, '路径未注册'))
|
||||||
|
elif permid not in perms_with_roles:
|
||||||
|
unauth.append((rel_path, '有记录但无角色'))
|
||||||
|
else:
|
||||||
|
authed += 1
|
||||||
|
|
||||||
|
by_module = defaultdict(list)
|
||||||
|
for rel_path, reason in unauth:
|
||||||
|
parts = rel_path.strip('/').split('/')
|
||||||
|
module = parts[0] if parts else 'root'
|
||||||
|
by_module[module].append((rel_path, reason))
|
||||||
|
|
||||||
|
html = (
|
||||||
|
f"<p><b>wwwroot:</b> {wwwroot}</p>"
|
||||||
|
f"<p><b>符号链接目录 ({len(symlinks)}):</b><br>{symlink_info}</p>"
|
||||||
|
f"<p>总文件数: {len(all_files)} | 已授权: {authed} | 未授权: {len(unauth)}</p>"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not unauth:
|
||||||
|
html += "<p style='color:#22C55E;font-weight:bold;'>所有文件均有权限覆盖,无需处理。</p>"
|
||||||
|
else:
|
||||||
|
html += f"<p style='color:#EF4444;font-weight:bold;'>未授权文件 ({len(unauth)} 个):</p>"
|
||||||
|
for module in sorted(by_module.keys()):
|
||||||
|
items = by_module[module]
|
||||||
|
html += f"<h4 style='color:#FBBF24;'>{module}/ ({len(items)} 个文件)</h4>"
|
||||||
|
html += "<ul style='font-family:monospace;font-size:12px;'>"
|
||||||
|
for rel_path, reason in items[:50]:
|
||||||
|
color = '#EF4444' if reason == '路径未注册' else '#F97316'
|
||||||
|
html += f"<li style='color:{color};'>[{reason}] {rel_path}</li>"
|
||||||
|
if len(items) > 50:
|
||||||
|
html += f"<li>... 还有 {len(items) - 50} 个文件</li>"
|
||||||
|
html += "</ul>"
|
||||||
|
|
||||||
|
return ('扫描结果', html, False)
|
||||||
@ -114,6 +114,31 @@ async def set_role_perms(dbname, module, orgtype, role, items):
|
|||||||
for tblname in items:
|
for tblname in items:
|
||||||
await set_role_perm(dbname, module, orgtype, role, tblname)
|
await set_role_perm(dbname, module, orgtype, role, tblname)
|
||||||
|
|
||||||
|
async def send_rbac_invalidation():
|
||||||
|
"""Send cache invalidation message to all processes via Redis Pub/Sub."""
|
||||||
|
try:
|
||||||
|
from ahserver.cache_sync import get_cache_sync
|
||||||
|
cache_sync = get_cache_sync()
|
||||||
|
# Use default Redis URL for CLI scripts
|
||||||
|
try:
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
env = ServerEnv()
|
||||||
|
redis_url = env.conf.website.session_redis.url
|
||||||
|
except (AttributeError, Exception):
|
||||||
|
redis_url = "redis://127.0.0.1:6379"
|
||||||
|
|
||||||
|
await cache_sync.start(redis_url)
|
||||||
|
# Invalidate both role-permission and all user caches
|
||||||
|
# (CLI scripts typically change permissions/roles)
|
||||||
|
await cache_sync.invalidate('rbac:rp')
|
||||||
|
await cache_sync.invalidate('rbac:ur:all')
|
||||||
|
debug('RBAC CLI: sent cache invalidation messages')
|
||||||
|
# Give a moment for the message to be published
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
await cache_sync.stop()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Warning: Failed to send cache invalidation: {e}')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
async def main():
|
async def main():
|
||||||
if len(sys.argv) < 6:
|
if len(sys.argv) < 6:
|
||||||
@ -124,6 +149,8 @@ if __name__ == '__main__':
|
|||||||
orgtype = sys.argv[3]
|
orgtype = sys.argv[3]
|
||||||
role = sys.argv[4]
|
role = sys.argv[4]
|
||||||
await set_role_perms(dbname, module, orgtype, role, sys.argv[5:])
|
await set_role_perms(dbname, module, orgtype, role, sys.argv[5:])
|
||||||
|
# Send invalidation message to all running Sage processes
|
||||||
|
await send_rbac_invalidation()
|
||||||
|
|
||||||
def run(coro):
|
def run(coro):
|
||||||
p = '.'
|
p = '.'
|
||||||
|
|||||||
59
rbac/user_stats.py
Normal file
59
rbac/user_stats.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from sqlor.dbpools import get_sor_context
|
||||||
|
from appPublic.timeUtils import curDateString, timestampstr
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from appPublic.log import debug, exception
|
||||||
|
|
||||||
|
async def get_user_stats(request):
|
||||||
|
"""Get user statistics for the platform"""
|
||||||
|
env = request._run_ns
|
||||||
|
today = curDateString()
|
||||||
|
tomorrow = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')
|
||||||
|
month_start = datetime.now().strftime('%Y-%m-01')
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
'total_users': 0,
|
||||||
|
'active_users_today': 0,
|
||||||
|
'new_users_this_month': 0,
|
||||||
|
'total_orgs': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async with get_sor_context(env, 'rbac') as sor:
|
||||||
|
# Total users
|
||||||
|
sql_users = """
|
||||||
|
SELECT COUNT(*) as cnt FROM users
|
||||||
|
"""
|
||||||
|
recs = await sor.sqlExe(sql_users, {})
|
||||||
|
if recs:
|
||||||
|
stats['total_users'] = int(recs[0].cnt or 0)
|
||||||
|
|
||||||
|
# Active users today (users with llmusage records today)
|
||||||
|
sql_active = """
|
||||||
|
SELECT COUNT(DISTINCT userid) as cnt FROM llmusage
|
||||||
|
WHERE use_date >= ${today}$
|
||||||
|
AND use_date < ${tomorrow}$
|
||||||
|
"""
|
||||||
|
recs = await sor.sqlExe(sql_active, {
|
||||||
|
'today': today,
|
||||||
|
'tomorrow': tomorrow
|
||||||
|
})
|
||||||
|
if recs:
|
||||||
|
stats['active_users_today'] = int(recs[0].cnt or 0)
|
||||||
|
|
||||||
|
# New users this month
|
||||||
|
sql_new = """
|
||||||
|
SELECT COUNT(*) as cnt FROM users
|
||||||
|
WHERE created_date >= ${month_start}$
|
||||||
|
"""
|
||||||
|
recs = await sor.sqlExe(sql_new, {'month_start': month_start})
|
||||||
|
if recs:
|
||||||
|
stats['new_users_this_month'] = int(recs[0].cnt or 0)
|
||||||
|
|
||||||
|
# Total organizations
|
||||||
|
sql_orgs = """
|
||||||
|
SELECT COUNT(*) as cnt FROM organization
|
||||||
|
"""
|
||||||
|
recs = await sor.sqlExe(sql_orgs, {})
|
||||||
|
if recs:
|
||||||
|
stats['total_orgs'] = int(recs[0].cnt or 0)
|
||||||
|
|
||||||
|
return stats
|
||||||
@ -4,6 +4,8 @@ from sqlor.dbpools import get_sor_context
|
|||||||
from ahserver.serverenv import ServerEnv
|
from ahserver.serverenv import ServerEnv
|
||||||
from appPublic.Singleton import SingletonDecorator
|
from appPublic.Singleton import SingletonDecorator
|
||||||
from appPublic.log import debug, error
|
from appPublic.log import debug, error
|
||||||
|
from ahserver.cache_sync import get_cache_sync
|
||||||
|
|
||||||
|
|
||||||
class LRUCache:
|
class LRUCache:
|
||||||
"""Async-safe LRU cache with TTL support.
|
"""Async-safe LRU cache with TTL support.
|
||||||
@ -81,82 +83,82 @@ class UserPermissions:
|
|||||||
# Async lock for rp_caches initialization (lazy init)
|
# Async lock for rp_caches initialization (lazy init)
|
||||||
self._rp_lock = None
|
self._rp_lock = None
|
||||||
|
|
||||||
def on_user_update(self, data):
|
async def on_user_update(self, data):
|
||||||
"""Event handler for users table update.
|
"""Event handler for users table update.
|
||||||
Clears the specific user's permission cache.
|
Clears the specific user's permission cache.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
userid = getattr(data, 'id', None)
|
userid = getattr(data, 'id', None)
|
||||||
if userid:
|
if userid:
|
||||||
self.invalidate_user_cache(userid)
|
await self.invalidate_user_cache(userid)
|
||||||
debug(f'RBAC cache invalidated for user id={userid} (users update)')
|
debug(f'RBAC cache invalidated for user id={userid} (users update)')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error(f'RBAC on_user_update handler error: {e}')
|
error(f'RBAC on_user_update handler error: {e}')
|
||||||
|
|
||||||
def on_user_create(self, data):
|
async def on_user_create(self, data):
|
||||||
"""Event handler for users table insert.
|
"""Event handler for users table insert.
|
||||||
Clears the specific user's permission cache.
|
Clears the specific user's permission cache.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
userid = getattr(data, 'id', None)
|
userid = getattr(data, 'id', None)
|
||||||
if userid:
|
if userid:
|
||||||
self.invalidate_user_cache(userid)
|
await self.invalidate_user_cache(userid)
|
||||||
debug(f'RBAC cache invalidated for user id={userid} (users create)')
|
debug(f'RBAC cache invalidated for user id={userid} (users create)')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error(f'RBAC on_user_create handler error: {e}')
|
error(f'RBAC on_user_create handler error: {e}')
|
||||||
|
|
||||||
def on_user_delete(self, data):
|
async def on_user_delete(self, data):
|
||||||
"""Event handler for users table delete.
|
"""Event handler for users table delete.
|
||||||
Clears the specific user's permission cache.
|
Clears the specific user's permission cache.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
userid = getattr(data, 'id', None)
|
userid = getattr(data, 'id', None)
|
||||||
if userid:
|
if userid:
|
||||||
self.invalidate_user_cache(userid)
|
await self.invalidate_user_cache(userid)
|
||||||
debug(f'RBAC cache invalidated for user id={userid} (users delete)')
|
debug(f'RBAC cache invalidated for user id={userid} (users delete)')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error(f'RBAC on_user_delete handler error: {e}')
|
error(f'RBAC on_user_delete handler error: {e}')
|
||||||
|
|
||||||
def on_rolepermission_change(self, data):
|
async def on_rolepermission_change(self, data):
|
||||||
"""Event handler for rolepermission table C/U/D.
|
"""Event handler for rolepermission table C/U/D.
|
||||||
Clears the role-permission cache.
|
Clears the role-permission cache.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.invalidate_rp_cache()
|
await self.invalidate_rp_cache()
|
||||||
debug('RBAC role-permission cache invalidated (rolepermission change)')
|
debug('RBAC role-permission cache invalidated (rolepermission change)')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error(f'RBAC on_rolepermission_change handler error: {e}')
|
error(f'RBAC on_rolepermission_change handler error: {e}')
|
||||||
|
|
||||||
def on_permission_change(self, data):
|
async def on_permission_change(self, data):
|
||||||
"""Event handler for permission table update.
|
"""Event handler for permission table update.
|
||||||
Clears the role-permission cache.
|
Clears the role-permission cache.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.invalidate_rp_cache()
|
await self.invalidate_rp_cache()
|
||||||
debug('RBAC role-permission cache invalidated (permission change)')
|
debug('RBAC role-permission cache invalidated (permission change)')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error(f'RBAC on_permission_change handler error: {e}')
|
error(f'RBAC on_permission_change handler error: {e}')
|
||||||
|
|
||||||
def on_role_change(self, data):
|
async def on_role_change(self, data):
|
||||||
"""Event handler for role table C/U/D.
|
"""Event handler for role table C/U/D.
|
||||||
Clears all user caches and role-permission cache,
|
Clears all user caches and role-permission cache,
|
||||||
since role changes may affect any user.
|
since role changes may affect any user.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.invalidate_all_user_caches()
|
await self.invalidate_all_user_caches()
|
||||||
self.invalidate_rp_cache()
|
await self.invalidate_rp_cache()
|
||||||
debug('RBAC all caches invalidated (role change)')
|
debug('RBAC all caches invalidated (role change)')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error(f'RBAC on_role_change handler error: {e}')
|
error(f'RBAC on_role_change handler error: {e}')
|
||||||
|
|
||||||
def on_userrole_change(self, data):
|
async def on_userrole_change(self, data):
|
||||||
"""Event handler for userrole table C/U/D.
|
"""Event handler for userrole table C/U/D.
|
||||||
Clears the specific user's permission cache based on userid.
|
Clears the specific user's permission cache based on userid.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
userid = getattr(data, 'userid', None)
|
userid = getattr(data, 'userid', None)
|
||||||
if userid:
|
if userid:
|
||||||
self.invalidate_user_cache(userid)
|
await self.invalidate_user_cache(userid)
|
||||||
debug(f'RBAC cache invalidated for user id={userid} (userrole change)')
|
debug(f'RBAC cache invalidated for user id={userid} (userrole change)')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error(f'RBAC on_userrole_change handler error: {e}')
|
error(f'RBAC on_userrole_change handler error: {e}')
|
||||||
@ -180,20 +182,37 @@ class UserPermissions:
|
|||||||
return self.ur_caches.get(userid)
|
return self.ur_caches.get(userid)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def invalidate_user_cache(self, userid):
|
async def invalidate_user_cache(self, userid):
|
||||||
"""Invalidate cache for a specific user.
|
"""Invalidate cache for a specific user.
|
||||||
Call this after role changes, user creation, etc.
|
Call this after role changes, user creation, etc.
|
||||||
|
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
|
||||||
"""
|
"""
|
||||||
self.ur_caches.invalidate(userid)
|
self.ur_caches.invalidate(userid)
|
||||||
|
# Broadcast to other processes
|
||||||
|
cache_sync = get_cache_sync()
|
||||||
|
if cache_sync.is_running:
|
||||||
|
await cache_sync.invalidate(f'rbac:ur:{userid}')
|
||||||
|
|
||||||
def invalidate_all_user_caches(self):
|
async def invalidate_all_user_caches(self):
|
||||||
"""Invalidate all user role caches."""
|
"""Invalidate all user role caches.
|
||||||
|
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
|
||||||
|
"""
|
||||||
self.ur_caches.clear()
|
self.ur_caches.clear()
|
||||||
|
# Broadcast to other processes
|
||||||
|
cache_sync = get_cache_sync()
|
||||||
|
if cache_sync.is_running:
|
||||||
|
await cache_sync.invalidate('rbac:ur:all')
|
||||||
|
|
||||||
def invalidate_rp_cache(self):
|
async def invalidate_rp_cache(self):
|
||||||
"""Invalidate role-permission cache (after permission changes)."""
|
"""Invalidate role-permission cache (after permission changes).
|
||||||
|
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
|
||||||
|
"""
|
||||||
self.rp_caches = None
|
self.rp_caches = None
|
||||||
self.rp_cache_loaded_at = 0
|
self.rp_cache_loaded_at = 0
|
||||||
|
# Broadcast to other processes
|
||||||
|
cache_sync = get_cache_sync()
|
||||||
|
if cache_sync.is_running:
|
||||||
|
await cache_sync.invalidate('rbac:rp')
|
||||||
|
|
||||||
async def load_roleperms(self, sor):
|
async def load_roleperms(self, sor):
|
||||||
"""Load all role-permission mappings into cache.
|
"""Load all role-permission mappings into cache.
|
||||||
|
|||||||
87
script/register_rbac_tools_perm.py
Normal file
87
script/register_rbac_tools_perm.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
注册 RBAC 工具的权限到数据库。
|
||||||
|
运行在 Sage Python 虚拟环境中。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
./py3/bin/python ../rbac/script/register_rbac_tools_perm.py
|
||||||
|
|
||||||
|
或在 Sage 根目录执行:
|
||||||
|
cd ~/repos/sage && ./py3/bin/python ../rbac/script/register_rbac_tools_perm.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# 确保 Sage 路径在 sys.path 中
|
||||||
|
sage_root = os.environ.get('SAGE_ROOT')
|
||||||
|
if sage_root and sage_root not in sys.path:
|
||||||
|
sys.path.insert(0, sage_root)
|
||||||
|
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
from appPublic.jsonConfig import getConfig
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
|
|
||||||
|
# 需要注册的权限列表: (path, role)
|
||||||
|
permissions = [
|
||||||
|
('/rbac/list_path_roles.ui', 'owner.superuser'),
|
||||||
|
('/rbac/list_path_roles.dspy', 'owner.superuser'),
|
||||||
|
('/rbac/find_unauth_files.dspy', 'owner.superuser'),
|
||||||
|
('/rbac/admin_menu.ui', 'owner.superuser'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
config = getConfig('.')
|
||||||
|
db = DBPools(config.databases)
|
||||||
|
registered = 0
|
||||||
|
|
||||||
|
async with db.sqlorContext('sage') as sor:
|
||||||
|
# 查找 superuser 角色 ID
|
||||||
|
role_recs = await sor.sqlExe(
|
||||||
|
"SELECT id FROM role WHERE orgtypeid='owner' AND name='superuser'", {}
|
||||||
|
)
|
||||||
|
if not role_recs:
|
||||||
|
print("错误: 未找到 owner.superuser 角色")
|
||||||
|
sys.exit(1)
|
||||||
|
superuser_id = role_recs[0].id
|
||||||
|
print(f"superuser role_id: {superuser_id}")
|
||||||
|
|
||||||
|
for path, role in permissions:
|
||||||
|
# 检查 permission 是否已存在
|
||||||
|
existing_perm = await sor.sqlExe(
|
||||||
|
"SELECT id FROM permission WHERE path=${path}$", {'path': path}
|
||||||
|
)
|
||||||
|
if existing_perm:
|
||||||
|
perm_id = existing_perm[0].id
|
||||||
|
print(f" permission 已存在: {path} (id={perm_id})")
|
||||||
|
else:
|
||||||
|
perm_id = getID()
|
||||||
|
await sor.C('permission', {'id': perm_id, 'path': path})
|
||||||
|
print(f" + permission: {path}")
|
||||||
|
|
||||||
|
# 检查 rolepermission 是否已存在
|
||||||
|
existing_rp = await sor.sqlExe(
|
||||||
|
"SELECT id FROM rolepermission WHERE roleid=${roleid}$ AND permid=${permid}$",
|
||||||
|
{'roleid': superuser_id, 'permid': perm_id}
|
||||||
|
)
|
||||||
|
if existing_rp:
|
||||||
|
print(f" rolepermission 已存在")
|
||||||
|
else:
|
||||||
|
await sor.C('rolepermission', {
|
||||||
|
'id': getID(),
|
||||||
|
'roleid': superuser_id,
|
||||||
|
'permid': perm_id
|
||||||
|
})
|
||||||
|
registered += 1
|
||||||
|
print(f" + rolepermission: superuser -> {path}")
|
||||||
|
|
||||||
|
print(f"\n共注册 {registered} 条新权限。")
|
||||||
|
if registered > 0:
|
||||||
|
print("请重启 Sage 以刷新权限缓存。")
|
||||||
|
else:
|
||||||
|
print("所有权限已存在,无需操作。")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.get_event_loop().run_until_complete(main())
|
||||||
@ -1,7 +1,17 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name":"users",
|
"name":"users",
|
||||||
"label":"用户管理"
|
"label":"用户管理",
|
||||||
"url":"{{entire_url('/rbac/users')}}"
|
"url":"{{entire_url('/rbac/users')}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"list_path_roles",
|
||||||
|
"label":"查询路径权限角色",
|
||||||
|
"url":"{{entire_url('/rbac/list_path_roles.ui')}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"find_unauth_files",
|
||||||
|
"label":"扫描未授权文件",
|
||||||
|
"url":"{{entire_url('/rbac/find_unauth_files.dspy')}}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
8
wwwroot/find_unauth_files.dspy
Normal file
8
wwwroot/find_unauth_files.dspy
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
wwwroot = params_kw.get('wwwroot', '').strip()
|
||||||
|
|
||||||
|
async with get_sor_context(request._run_ns, 'rbac') as sor:
|
||||||
|
title, message, is_error = await request._run_ns.scan_unauth_files(sor, wwwroot, __file__)
|
||||||
|
|
||||||
|
if is_error:
|
||||||
|
return UiError(title=title, message=message)
|
||||||
|
return UiMessage(title=title, message=message)
|
||||||
212
wwwroot/index.ui
Normal file
212
wwwroot/index.ui
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
{% set roles = get_user_roles(get_user()) %}
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"height": "100%",
|
||||||
|
"padding": "0",
|
||||||
|
"bgcolor": "#0B1120"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"alignItems": "center",
|
||||||
|
"marginBottom": "24px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Title2",
|
||||||
|
"options": {
|
||||||
|
"text": "用户与权限",
|
||||||
|
"color": "#F1F5F9",
|
||||||
|
"fontWeight": "700"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Filler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "用户管理、角色权限与安全审计",
|
||||||
|
"fontSize": "14px",
|
||||||
|
"color": "#64748B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{% if 'reseller.admin' in roles or 'owner.superuser' in roles %}
|
||||||
|
{
|
||||||
|
"widgettype": "ResponsableBox",
|
||||||
|
"options": {
|
||||||
|
"gap": "16px",
|
||||||
|
"minWidth": "250px",
|
||||||
|
"marginBottom": "24px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"bgcolor": "#1E293B",
|
||||||
|
"padding": "24px",
|
||||||
|
"borderRadius": "12px",
|
||||||
|
"border": "1px solid #334155",
|
||||||
|
"cursor": "pointer"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.rbac_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('/rbac/users')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Svg",
|
||||||
|
"options": {
|
||||||
|
"svg": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#60A5FA\" stroke-width=\"1.5\"><path d=\"M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.953 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z\"/></svg>",
|
||||||
|
"width": "36px",
|
||||||
|
"height": "36px",
|
||||||
|
"marginBottom": "16px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Title4",
|
||||||
|
"options": {
|
||||||
|
"text": "用户管理",
|
||||||
|
"color": "#F1F5F9",
|
||||||
|
"fontWeight": "600",
|
||||||
|
"marginBottom": "8px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "管理系统用户、角色分配与账户信息",
|
||||||
|
"fontSize": "14px",
|
||||||
|
"color": "#94A3B8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"bgcolor": "#1E293B",
|
||||||
|
"padding": "24px",
|
||||||
|
"borderRadius": "12px",
|
||||||
|
"border": "1px solid #334155",
|
||||||
|
"cursor": "pointer"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.rbac_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('/rbac/list_path_roles.ui')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Svg",
|
||||||
|
"options": {
|
||||||
|
"svg": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#A78BFA\" stroke-width=\"1.5\"><path d=\"M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z\"/></svg>",
|
||||||
|
"width": "36px",
|
||||||
|
"height": "36px",
|
||||||
|
"marginBottom": "16px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Title4",
|
||||||
|
"options": {
|
||||||
|
"text": "路径权限角色",
|
||||||
|
"color": "#F1F5F9",
|
||||||
|
"fontWeight": "600",
|
||||||
|
"marginBottom": "8px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "查询各路径绑定的角色与权限配置",
|
||||||
|
"fontSize": "14px",
|
||||||
|
"color": "#94A3B8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"bgcolor": "#1E293B",
|
||||||
|
"padding": "24px",
|
||||||
|
"borderRadius": "12px",
|
||||||
|
"border": "1px solid #334155",
|
||||||
|
"cursor": "pointer"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.rbac_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('/rbac/find_unauth_files.dspy')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Svg",
|
||||||
|
"options": {
|
||||||
|
"svg": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#EF4444\" stroke-width=\"1.5\"><path d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"/></svg>",
|
||||||
|
"width": "36px",
|
||||||
|
"height": "36px",
|
||||||
|
"marginBottom": "16px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Title4",
|
||||||
|
"options": {
|
||||||
|
"text": "扫描未授权文件",
|
||||||
|
"color": "#F1F5F9",
|
||||||
|
"fontWeight": "600",
|
||||||
|
"marginBottom": "8px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "检测未配置RBAC权限的页面文件",
|
||||||
|
"fontSize": "14px",
|
||||||
|
"color": "#94A3B8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{% endif %}
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"id": "rbac_content",
|
||||||
|
"css": "filler",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"overflowY": "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
8
wwwroot/list_path_roles.dspy
Normal file
8
wwwroot/list_path_roles.dspy
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
path = params_kw.get('path', '').strip()
|
||||||
|
|
||||||
|
async with get_sor_context(request._run_ns, 'rbac') as sor:
|
||||||
|
title, message, is_error = await request._run_ns.query_path_roles(sor, path)
|
||||||
|
|
||||||
|
if is_error:
|
||||||
|
return UiError(title=title, message=message)
|
||||||
|
return UiMessage(title=title, message=message)
|
||||||
27
wwwroot/list_path_roles.ui
Normal file
27
wwwroot/list_path_roles.ui
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"widgettype": "ModalForm",
|
||||||
|
"options": {
|
||||||
|
"cwidth": 20,
|
||||||
|
"cheight": 15,
|
||||||
|
"title": "查询路径权限角色",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "path",
|
||||||
|
"label": "路径",
|
||||||
|
"uitype": "str",
|
||||||
|
"placeholder": "如 /harnessed_agent/index.ui"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "submit",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "root.page_center",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('./list_path_roles.dspy')}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
53
wwwroot/stat_active_users.ui
Normal file
53
wwwroot/stat_active_users.ui
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{% set stats = get_user_stats(request) %}
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"bgcolor": "#1E293B",
|
||||||
|
"padding": "20px",
|
||||||
|
"borderRadius": "12px",
|
||||||
|
"border": "1px solid #334155",
|
||||||
|
"flex": "1",
|
||||||
|
"minHeight": "110px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {
|
||||||
|
"alignItems": "center",
|
||||||
|
"marginBottom": "12px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Svg",
|
||||||
|
"options": {
|
||||||
|
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#22C55E\" stroke-width=\"2\"><path d=\"M15.91 11.672a.375.375 0 010 .656l-5.603 3.113a.375.375 0 01-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112z\"/><path d=\"M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"/></svg>",
|
||||||
|
"width": "24px",
|
||||||
|
"height": "24px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Filler"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "{{stats.active_users_today}}",
|
||||||
|
"fontSize": "32px",
|
||||||
|
"fontWeight": "700",
|
||||||
|
"color": "#F1F5F9",
|
||||||
|
"lineHeight": "1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "今日活跃用户",
|
||||||
|
"fontSize": "14px",
|
||||||
|
"color": "#94A3B8",
|
||||||
|
"marginTop": "4px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
53
wwwroot/stat_total_orgs.ui
Normal file
53
wwwroot/stat_total_orgs.ui
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{% set stats = get_user_stats(request) %}
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"bgcolor": "#1E293B",
|
||||||
|
"padding": "20px",
|
||||||
|
"borderRadius": "12px",
|
||||||
|
"border": "1px solid #334155",
|
||||||
|
"flex": "1",
|
||||||
|
"minHeight": "110px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {
|
||||||
|
"alignItems": "center",
|
||||||
|
"marginBottom": "12px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Svg",
|
||||||
|
"options": {
|
||||||
|
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#8B5CF6\" stroke-width=\"2\"><path d=\"M19 7.5v3m0 0v3m0 0v3m0 0v3m0 0h-3m0 0h-3m0 0h-3m0 0h-3m0 0v-3m0 0V12m0 0V7.5M5 21h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z\"/></svg>",
|
||||||
|
"width": "24px",
|
||||||
|
"height": "24px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Filler"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "{{stats.total_orgs}}",
|
||||||
|
"fontSize": "32px",
|
||||||
|
"fontWeight": "700",
|
||||||
|
"color": "#F1F5F9",
|
||||||
|
"lineHeight": "1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "组织机构数",
|
||||||
|
"fontSize": "14px",
|
||||||
|
"color": "#94A3B8",
|
||||||
|
"marginTop": "4px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
53
wwwroot/stat_total_users.ui
Normal file
53
wwwroot/stat_total_users.ui
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{% set stats = get_user_stats(request) %}
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"bgcolor": "#1E293B",
|
||||||
|
"padding": "20px",
|
||||||
|
"borderRadius": "12px",
|
||||||
|
"border": "1px solid #334155",
|
||||||
|
"flex": "1",
|
||||||
|
"minHeight": "110px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {
|
||||||
|
"alignItems": "center",
|
||||||
|
"marginBottom": "12px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Svg",
|
||||||
|
"options": {
|
||||||
|
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3B82F6\" stroke-width=\"2\"><path d=\"M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z\"/></svg>",
|
||||||
|
"width": "24px",
|
||||||
|
"height": "24px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Filler"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "{{stats.total_users}}",
|
||||||
|
"fontSize": "32px",
|
||||||
|
"fontWeight": "700",
|
||||||
|
"color": "#F1F5F9",
|
||||||
|
"lineHeight": "1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "总用户数",
|
||||||
|
"fontSize": "14px",
|
||||||
|
"color": "#94A3B8",
|
||||||
|
"marginTop": "4px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user