Compare commits
No commits in common. "835a2ff3f7a1857867417df2b6b85293bdc01632" and "fd9ef322c7fa3eb2b899ce31e318d6e193599f9f" have entirely different histories.
835a2ff3f7
...
fd9ef322c7
30
rbac/init.py
30
rbac/init.py
@ -5,11 +5,6 @@ from .orgs import (
|
||||
get_platform_providers
|
||||
)
|
||||
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 (
|
||||
objcheckperm,
|
||||
get_org_users,
|
||||
@ -26,7 +21,6 @@ from rbac.set_role_perms import (
|
||||
set_role_perms
|
||||
)
|
||||
from appPublic.log import debug
|
||||
from ahserver.cache_sync import get_cache_sync
|
||||
|
||||
async def get_owner_orgid(*args, **kw):
|
||||
return '0'
|
||||
@ -65,27 +59,6 @@ def _bind_rbac_events(dbpools, dbname, up):
|
||||
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():
|
||||
AuthAPI.checkUserPermission = objcheckperm
|
||||
env = ServerEnv()
|
||||
@ -103,9 +76,6 @@ def load_rbac():
|
||||
env.sor_get_org_users = sor_get_org_users
|
||||
env.get_owner_orgid = get_owner_orgid
|
||||
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
|
||||
env.invalidate_user_perm_cache = env.userpermissions.invalidate_user_cache
|
||||
env.invalidate_all_perm_caches = env.userpermissions.invalidate_all_user_caches
|
||||
|
||||
@ -1,228 +0,0 @@
|
||||
"""
|
||||
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,31 +114,6 @@ async def set_role_perms(dbname, module, orgtype, role, items):
|
||||
for tblname in items:
|
||||
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__':
|
||||
async def main():
|
||||
if len(sys.argv) < 6:
|
||||
@ -149,8 +124,6 @@ if __name__ == '__main__':
|
||||
orgtype = sys.argv[3]
|
||||
role = sys.argv[4]
|
||||
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):
|
||||
p = '.'
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
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,8 +4,6 @@ from sqlor.dbpools import get_sor_context
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.Singleton import SingletonDecorator
|
||||
from appPublic.log import debug, error
|
||||
from ahserver.cache_sync import get_cache_sync
|
||||
|
||||
|
||||
class LRUCache:
|
||||
"""Async-safe LRU cache with TTL support.
|
||||
@ -83,82 +81,82 @@ class UserPermissions:
|
||||
# Async lock for rp_caches initialization (lazy init)
|
||||
self._rp_lock = None
|
||||
|
||||
async def on_user_update(self, data):
|
||||
def on_user_update(self, data):
|
||||
"""Event handler for users table update.
|
||||
Clears the specific user's permission cache.
|
||||
"""
|
||||
try:
|
||||
userid = getattr(data, 'id', None)
|
||||
if userid:
|
||||
await self.invalidate_user_cache(userid)
|
||||
self.invalidate_user_cache(userid)
|
||||
debug(f'RBAC cache invalidated for user id={userid} (users update)')
|
||||
except Exception as e:
|
||||
error(f'RBAC on_user_update handler error: {e}')
|
||||
|
||||
async def on_user_create(self, data):
|
||||
def on_user_create(self, data):
|
||||
"""Event handler for users table insert.
|
||||
Clears the specific user's permission cache.
|
||||
"""
|
||||
try:
|
||||
userid = getattr(data, 'id', None)
|
||||
if userid:
|
||||
await self.invalidate_user_cache(userid)
|
||||
self.invalidate_user_cache(userid)
|
||||
debug(f'RBAC cache invalidated for user id={userid} (users create)')
|
||||
except Exception as e:
|
||||
error(f'RBAC on_user_create handler error: {e}')
|
||||
|
||||
async def on_user_delete(self, data):
|
||||
def on_user_delete(self, data):
|
||||
"""Event handler for users table delete.
|
||||
Clears the specific user's permission cache.
|
||||
"""
|
||||
try:
|
||||
userid = getattr(data, 'id', None)
|
||||
if userid:
|
||||
await self.invalidate_user_cache(userid)
|
||||
self.invalidate_user_cache(userid)
|
||||
debug(f'RBAC cache invalidated for user id={userid} (users delete)')
|
||||
except Exception as e:
|
||||
error(f'RBAC on_user_delete handler error: {e}')
|
||||
|
||||
async def on_rolepermission_change(self, data):
|
||||
def on_rolepermission_change(self, data):
|
||||
"""Event handler for rolepermission table C/U/D.
|
||||
Clears the role-permission cache.
|
||||
"""
|
||||
try:
|
||||
await self.invalidate_rp_cache()
|
||||
self.invalidate_rp_cache()
|
||||
debug('RBAC role-permission cache invalidated (rolepermission change)')
|
||||
except Exception as e:
|
||||
error(f'RBAC on_rolepermission_change handler error: {e}')
|
||||
|
||||
async def on_permission_change(self, data):
|
||||
def on_permission_change(self, data):
|
||||
"""Event handler for permission table update.
|
||||
Clears the role-permission cache.
|
||||
"""
|
||||
try:
|
||||
await self.invalidate_rp_cache()
|
||||
self.invalidate_rp_cache()
|
||||
debug('RBAC role-permission cache invalidated (permission change)')
|
||||
except Exception as e:
|
||||
error(f'RBAC on_permission_change handler error: {e}')
|
||||
|
||||
async def on_role_change(self, data):
|
||||
def on_role_change(self, data):
|
||||
"""Event handler for role table C/U/D.
|
||||
Clears all user caches and role-permission cache,
|
||||
since role changes may affect any user.
|
||||
"""
|
||||
try:
|
||||
await self.invalidate_all_user_caches()
|
||||
await self.invalidate_rp_cache()
|
||||
self.invalidate_all_user_caches()
|
||||
self.invalidate_rp_cache()
|
||||
debug('RBAC all caches invalidated (role change)')
|
||||
except Exception as e:
|
||||
error(f'RBAC on_role_change handler error: {e}')
|
||||
|
||||
async def on_userrole_change(self, data):
|
||||
def on_userrole_change(self, data):
|
||||
"""Event handler for userrole table C/U/D.
|
||||
Clears the specific user's permission cache based on userid.
|
||||
"""
|
||||
try:
|
||||
userid = getattr(data, 'userid', None)
|
||||
if userid:
|
||||
await self.invalidate_user_cache(userid)
|
||||
self.invalidate_user_cache(userid)
|
||||
debug(f'RBAC cache invalidated for user id={userid} (userrole change)')
|
||||
except Exception as e:
|
||||
error(f'RBAC on_userrole_change handler error: {e}')
|
||||
@ -182,37 +180,20 @@ class UserPermissions:
|
||||
return self.ur_caches.get(userid)
|
||||
return None
|
||||
|
||||
async def invalidate_user_cache(self, userid):
|
||||
def invalidate_user_cache(self, userid):
|
||||
"""Invalidate cache for a specific user.
|
||||
Call this after role changes, user creation, etc.
|
||||
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
|
||||
"""
|
||||
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}')
|
||||
|
||||
async def invalidate_all_user_caches(self):
|
||||
"""Invalidate all user role caches.
|
||||
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
|
||||
"""
|
||||
def invalidate_all_user_caches(self):
|
||||
"""Invalidate all user role caches."""
|
||||
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')
|
||||
|
||||
async def invalidate_rp_cache(self):
|
||||
"""Invalidate role-permission cache (after permission changes).
|
||||
Also broadcasts invalidation to all other processes via Redis Pub/Sub.
|
||||
"""
|
||||
def invalidate_rp_cache(self):
|
||||
"""Invalidate role-permission cache (after permission changes)."""
|
||||
self.rp_caches = None
|
||||
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):
|
||||
"""Load all role-permission mappings into cache.
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
#!/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,17 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name":"users",
|
||||
"label":"用户管理",
|
||||
"label":"用户管理"
|
||||
"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')}}"
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
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
212
wwwroot/index.ui
@ -1,212 +0,0 @@
|
||||
{% 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
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)
|
||||
@ -1,27 +0,0 @@
|
||||
{
|
||||
"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')}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
{% 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
{% 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
{% 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