feat: add scripts/load_path.py for RBAC permission initialization

- Add scripts/load_path.py with all 28 wwwroot paths
- menu.ui set to 'any' role (public nav access)
- All other paths set to 'logined' role (dashboard visible to authenticated users)
- Idempotent: skips already-registered paths
This commit is contained in:
yumoqing 2026-05-25 22:24:48 +08:00
parent e7fc646372
commit fd8443f445

141
scripts/load_path.py Normal file
View File

@ -0,0 +1,141 @@
"""Generate RBAC permissions for dashboard_for_sage module paths.
Run from Sage root with Sage venv:
cd ~/repos/sage && ./py3/bin/python ../dashboard_for_sage/scripts/load_path.py
Or set SAGE_ROOT environment variable.
"""
import os
import sys
import asyncio
# Ensure Sage root is in 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.dictObject import DictObject
from appPublic.uniqueID import getID
# ── Permission definitions ──
# Format: (path, role)
# Dashboard files are accessible to all logined users except menu.ui (any)
paths = [
# Module root and index
("/dashboard_for_sage", "logined"),
("/dashboard_for_sage/index.ui", "logined"),
# Menu — must be any so unauthenticated users can see nav
("/dashboard_for_sage/menu.ui", "any"),
# Shell / theme files
("/dashboard_for_sage/shell.ui", "logined"),
("/dashboard_for_sage/shell_theme.css", "logined"),
("/dashboard_for_sage/shell_theme.js", "logined"),
# Global menu
("/dashboard_for_sage/global_menu.ui", "logined"),
# Stat cards
("/dashboard_for_sage/stat_today_usage.ui", "logined"),
("/dashboard_for_sage/stat_today_amount.ui", "logined"),
("/dashboard_for_sage/stat_total_users.ui", "logined"),
("/dashboard_for_sage/stat_active_users.ui", "logined"),
("/dashboard_for_sage/stat_concurrent.ui", "logined"),
("/dashboard_for_sage/stat_errors.ui", "logined"),
("/dashboard_for_sage/stat_new_users_month.ui", "logined"),
("/dashboard_for_sage/stat_total_orgs.ui", "logined"),
# Legacy stat cards (backward compat)
("/dashboard_for_sage/today_usage.ui", "logined"),
("/dashboard_for_sage/today_amount.ui", "logined"),
("/dashboard_for_sage/total_users.ui", "logined"),
("/dashboard_for_sage/concurrent_users.ui", "logined"),
("/dashboard_for_sage/accounting_errors.ui", "logined"),
# Top 5 ranking cards
("/dashboard_for_sage/table_top_users.ui", "logined"),
("/dashboard_for_sage/table_top_users_amount.ui", "logined"),
("/dashboard_for_sage/table_top_users_count.ui", "logined"),
("/dashboard_for_sage/table_top_providers_amount.ui", "logined"),
("/dashboard_for_sage/table_top_providers_count.ui", "logined"),
("/dashboard_for_sage/top_users_amount.ui", "logined"),
# Charts
("/dashboard_for_sage/chart_top_models.ui", "logined"),
("/dashboard_for_sage/top_models_chart.ui", "logined"),
# API endpoints
("/dashboard_for_sage/api/top_models.dspy", "logined"),
]
async def add_roleperm(sor, roleid, permid):
"""Add role-permission mapping if not exists."""
ns = {'roleid': roleid, 'permid': permid}
recs = await sor.R('rolepermission', ns.copy())
if not recs:
ns['id'] = getID()
await sor.C('rolepermission', ns.copy())
async def add_roles_perm(sor, perm, roles):
"""Register permission for special roles."""
if roles in [['any'], ['anonymous'], ['logined']]:
role = roles[0]
await add_roleperm(sor, role, perm.id)
return
for role in roles:
if '.' in role:
orgtypeid, name = role.split('.', 1)
else:
orgtypeid, name = '*', role
ns = {'orgtypeid': orgtypeid, 'name': name}
roles_rec = await sor.R('role', ns.copy())
if not roles_rec:
ns['id'] = getID()
await sor.C('role', ns.copy())
else:
ns['id'] = roles_rec[0].id
await add_roleperm(sor, ns['id'], perm.id)
# Remove 'any' fallback for this perm
ns_any = {'roleid': 'any', 'permid': perm.id}
existing = await sor.R('rolepermission', ns_any.copy())
if existing:
await sor.D('rolepermission', {'id': existing[0].id})
async def main():
config = getConfig('.')
db = DBPools(config.databases)
cnt = 0
async with db.sqlorContext('sage') as sor:
for path, role in paths:
ns = {'path': path}
recs = await sor.R('permission', ns.copy())
if recs:
# Permission exists, skip (idempotent)
continue
cnt += 1
pid = getID()
ns['id'] = pid
await sor.C('permission', ns.copy())
perm = DictObject(**ns)
await add_roles_perm(sor, perm, [role])
print(f'{cnt} path(s) inserted for dashboard_for_sage')
if cnt == 0:
print('All paths already registered — no changes needed.')
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())