feat: add RBAC tools — list_path_roles, find_unauth_files, and permission registration script
This commit is contained in:
parent
c53c16d54c
commit
0b456486db
@ -5,6 +5,7 @@ from .orgs import (
|
||||
get_platform_providers
|
||||
)
|
||||
from .userperm import UserPermissions
|
||||
from .user_stats import get_user_stats
|
||||
from rbac.check_perm import (
|
||||
objcheckperm,
|
||||
get_org_users,
|
||||
@ -76,6 +77,7 @@ 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
|
||||
# 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
|
||||
|
||||
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
|
||||
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())
|
||||
@ -2,25 +2,36 @@
|
||||
import os
|
||||
|
||||
wwwroot = params_kw.get('wwwroot', '').strip()
|
||||
if not wwwroot:
|
||||
wwwroot = None
|
||||
|
||||
from appPublic.dictObject import DictObject
|
||||
|
||||
# 定位 wwwroot
|
||||
if not wwwroot:
|
||||
# 默认: 当前模块 wwwroot 的父目录 wwwroot
|
||||
# rbac/wwwroot -> 找 sage/wwwroot
|
||||
# 如果 sage_root 环境变量存在
|
||||
sage_root = os.environ.get('SAGE_ROOT')
|
||||
if sage_root:
|
||||
wwwroot = os.path.join(sage_root, 'wwwroot')
|
||||
else:
|
||||
# 尝试从当前文件路径推断: rbac/wwwroot/xxx.dspy -> sage/wwwroot
|
||||
# 从当前文件路径向上找 wwwroot
|
||||
# 生产: ~/token/sage/wwwroot/rbac/find_unauth_files.dspy
|
||||
# 开发: ~/repos/sage/wwwroot/rbac/find_unauth_files.dspy (通过symlink)
|
||||
# 或: ~/repos/rbac/wwwroot/find_unauth_files.dspy (rbac repo自身)
|
||||
this_file = os.path.abspath(__file__)
|
||||
# 通常在 repos/sage/wwwroot/rbac/find_unauth_files.dspy
|
||||
# wwwroot 是上一层
|
||||
wwwroot = os.path.dirname(os.path.dirname(this_file))
|
||||
# 如果路径中包含 /wwwroot/ 段,直接截断
|
||||
if '/wwwroot/' in this_file:
|
||||
idx = this_file.index('/wwwroot/')
|
||||
wwwroot = this_file[:idx + len('/wwwroot')]
|
||||
else:
|
||||
# 尝试找 wwwroot 子目录
|
||||
for level in range(5):
|
||||
candidate = this_file
|
||||
for _ in range(level):
|
||||
candidate = os.path.dirname(candidate)
|
||||
if not candidate or candidate == '/':
|
||||
break
|
||||
wr = os.path.join(candidate, 'wwwroot')
|
||||
if os.path.isdir(wr):
|
||||
wwwroot = wr
|
||||
break
|
||||
if not wwwroot:
|
||||
return UiError(title='错误', message='无法自动定位 wwwroot 目录,请通过参数传入 wwwroot 路径,或设置 SAGE_ROOT 环境变量。')
|
||||
|
||||
wwwroot = os.path.abspath(wwwroot)
|
||||
if not os.path.isdir(wwwroot):
|
||||
|
||||
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