This commit is contained in:
yumoqing 2026-05-03 14:26:27 +08:00
parent de8ce23fcb
commit f932bfb088
3 changed files with 273 additions and 41 deletions

View File

@ -12,11 +12,87 @@ Usage:
import os
import sys
import asyncio
import fnmatch
from appPublic.uniqueID import getID
from appPublic.log import debug, info
from appPublic.jsonConfig import getConfig
from sqlor.dbpools import DBPools
def _scan_wwwroot_paths(wwwroot_dir, module_dir):
"""Scan wwwroot directory to get all actual file paths for a module.
Returns list of relative paths like '/customer_management/customer_list.ui'
"""
full_dir = os.path.join(wwwroot_dir, module_dir)
if not os.path.isdir(full_dir):
return []
paths = []
for root, dirs, files in os.walk(full_dir):
for f in files:
abs_path = os.path.join(root, f)
rel_path = os.path.relpath(abs_path, wwwroot_dir)
# Convert to URL path (forward slashes, leading /)
url_path = '/' + rel_path.replace(os.sep, '/')
paths.append(url_path)
return sorted(paths)
def _expand_wildcard_pattern(pattern, wwwroot_dir, all_paths):
"""Expand a wildcard pattern like '/customer_management/**' to actual paths.
Since rbac.check_roles_path() only does exact match, we need to expand
** patterns into individual file paths.
"""
if '**' not in pattern:
return [pattern]
# Pattern like '/customer_management/**' or '/financial_management/invoice**'
prefix = pattern.replace('**', '').rstrip('/')
matched = []
for p in all_paths:
if p.startswith(prefix):
matched.append(p)
return matched if matched else [pattern]
def _expand_permissions(PERMISSION_MATRIX, wwwroot_dir):
"""Expand PERMISSION_MATRIX with wildcard patterns into actual file paths.
Returns dict: {role_id: set_of_paths}
"""
# Build master list of all actual file paths
all_paths = []
if os.path.isdir(wwwroot_dir):
for item in os.listdir(wwwroot_dir):
full_item = os.path.join(wwwroot_dir, item)
if os.path.isdir(full_item):
all_paths.extend(_scan_wwwroot_paths(wwwroot_dir, item))
info(f'Scanned {len(all_paths)} actual file paths from wwwroot')
# Expand patterns
role_paths = {}
for module, patterns in PERMISSION_MATRIX.items():
for pattern, role_list in patterns.items():
if '**' in pattern:
expanded = _expand_wildcard_pattern(pattern, wwwroot_dir, all_paths)
info(f' Expanded "{pattern}" -> {len(expanded)} paths')
else:
expanded = [pattern]
for path in expanded:
if path not in role_paths:
role_paths[path] = set()
for role in role_list:
role_paths[path].add(role)
return role_paths
async def ensure_permission(sor, path, name='', permtype='page'):
"""Ensure a permission exists in the database."""
recs = await sor.R('permission', {'path': path})
@ -33,10 +109,24 @@ async def ensure_permission(sor, path, name='', permtype='page'):
async def ensure_role(sor, roleid, name, desc=''):
"""Ensure a role exists with the given ID."""
"""Ensure a role exists with the given ID.
If a role with the same name or roleid-as-name already exists, reuse its ID.
This handles the case where roles were pre-created with UUID IDs but we define
them with string IDs in perm_config.py.
"""
# Try matching by name (supports both Chinese and English names)
for match_name in [name, roleid]:
recs = await sor.R('role', {'name': match_name})
if recs:
return recs[0].id
# Try matching by ID
recs = await sor.R('role', {'id': roleid})
if recs:
return recs[0].id
# Create new role
await sor.C('role', {
'id': roleid,
'orgtypeid': '*',
@ -62,8 +152,9 @@ async def init_permissions_from_config(dbname, config_module=None):
CRM是单业主机构系统所有角色属于同一业主机构
流程
1. 创建约定角色any/logined/anonymous和定义的角色
2. 注册 PERMISSION_MATRIX 中的路径权限
3. 注册 CRUD 路径并授权
2. 扫描 wwwroot 目录展开 ** 通配符为实际文件路径
3. 注册权限并授权
4. 注册 CRUD 路径并授权
"""
if config_module is None:
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
@ -75,13 +166,20 @@ async def init_permissions_from_config(dbname, config_module=None):
PERMISSION_MATRIX = config_module.PERMISSION_MATRIX
CRUD_TABLES = config_module.CRUD_TABLES
# Get wwwroot directory from config
config = getConfig()
workdir = os.path.dirname(os.path.dirname(__file__))
wwwroot_dir = os.path.join(workdir, 'wwwroot')
# Expand wildcards into actual file paths
role_paths = _expand_permissions(PERMISSION_MATRIX, wwwroot_dir)
db = DBPools()
async with db.sqlorContext(dbname) as sor:
# Step 1: 创建约定角色固定IDrbac/userperm.py硬编码检查
info('Creating convention roles...')
role_ids = {}
# 约定角色必须用固定ID
for fixed_id in ['any', 'logined', 'anonymous']:
role_ids[fixed_id] = await ensure_role(sor, fixed_id, fixed_id)
@ -92,16 +190,22 @@ async def init_permissions_from_config(dbname, config_module=None):
sor, role['id'], role['name'], role.get('desc', '')
)
# Step 3: 注册权限矩阵中的路径并授权
info('Registering permissions from matrix...')
# Step 3: 注册展开后的路径权限并授权
info('Registering expanded permissions...')
perm_count = 0
for module, paths in PERMISSION_MATRIX.items():
for path_pattern, role_list in paths.items():
permid = await ensure_permission(sor, path_pattern,
name=f'{module}: {path_pattern}', permtype='module')
for path, roles in role_paths.items():
# Register both /xxx and /main/xxx variants
# The auth middleware passes request.path (e.g. /main/customer_management/xxx.ui)
# but perm_config.py defines paths without /main prefix
variants = [path]
if not path.startswith('/main/'):
variants.append('/main' + path)
for variant in variants:
permid = await ensure_permission(sor, variant, permtype='page')
for role_name in role_list:
for role_name in roles:
if role_name in role_ids:
await grant_permission(sor, role_ids[role_name], permid)
perm_count += 1
@ -114,7 +218,7 @@ async def init_permissions_from_config(dbname, config_module=None):
permid = await ensure_permission(sor, crud_path,
name=f'{module}/{table} CRUD', permtype='crud')
# 根据模块权限矩阵授予权限
# Grant to roles that have module-level access
if module in PERMISSION_MATRIX:
for path_pattern, role_list in PERMISSION_MATRIX[module].items():
if path_pattern.startswith(f'/{module}'):
@ -124,12 +228,42 @@ async def init_permissions_from_config(dbname, config_module=None):
perm_count += 1
info(f'Permission initialization complete: {perm_count} grants created')
# Step 5: 同步 admin 和 superuser 角色权限(兼容已有数据库)
# 关键superadmin 用户的角色是 UUID-based (orgtypeid=customer),需要额外同步
info('Syncing admin and superuser role permissions...')
admin_role_id = role_ids.get('admin')
superuser_role_id = role_ids.get('superuser')
admin_super_id = role_ids.get('admin_superuser')
# Get admin_superuser grants (source of truth)
admin_super_perms = []
if admin_super_id:
admin_super_perms = await sor.R('rolepermission', {'roleid': admin_super_id})
for g in admin_super_perms:
if admin_role_id:
await grant_permission(sor, admin_role_id, g.permid)
perm_count += 1
if superuser_role_id:
await grant_permission(sor, superuser_role_id, g.permid)
perm_count += 1
# 额外同步:找到所有 orgtypeid=customer 的 admin/superuser 角色并授权
info('Syncing customer-org admin/superuser roles...')
all_roles = await sor.R('role', {'orgtypeid': 'customer'})
for r in all_roles:
if r.name in ('admin', 'superuser'):
for g in admin_super_perms:
await grant_permission(sor, r.id, g.permid)
perm_count += 1
info(f' Synced {len(admin_super_perms)} grants to role {r.id} (name={r.name})')
info(f'After sync: {perm_count} total grants created')
def main():
"""Run permission initialization."""
import json
from appPublic.jsonConfig import getConfig
from appPublic.dictObject import DictObject
config_path = os.path.dirname(os.path.dirname(__file__))

View File

@ -25,7 +25,9 @@ ROLES = [
# 财务部
{'id': 'finance_admin', 'name': '财务管理员', 'desc': '所有财务操作、报表'},
{'id': 'finance_clerk', 'name': '财务出纳', 'desc': '收款登记、付款处理'},
# 管理员
# 管理员(兼容已有数据库中的角色名)
{'id': 'admin', 'name': '管理员', 'desc': '全部权限'},
{'id': 'superuser', 'name': '超级用户', 'desc': '全部权限'},
{'id': 'admin_superuser', 'name': '超级用户', 'desc': '全部权限,初始化用'},
]
@ -35,13 +37,38 @@ ROLES = [
# ============================================================
PERMISSION_MATRIX = {
# ----------------------------------------------------------
# 公共路径(登录等)
# 静态资源bricks 框架)—— 任何人可访问
# ----------------------------------------------------------
'main': {
'bricks': {
'/bricks/**': ['any'],
},
# ----------------------------------------------------------
# 公共路径(登录等)—— 任何人可访问
# ----------------------------------------------------------
'public': {
'/main/rbac/user/login.ui': ['any'],
'/main/rbac/user/login.dspy': ['any'],
'/main/rbac/user/up_login.dspy': ['any'],
'/main/rbac/user/logout.dspy': ['any'],
'/main/rbac/user/register.dspy': ['any'],
'/main/rbac/user/register.ui': ['any'],
'/main/login.ui': ['any'],
'/main/login.dspy': ['any'],
},
# ----------------------------------------------------------
# 根路径 —— 已登录用户可访问(/main/ 指向 base.ui
# ----------------------------------------------------------
'root': {
'/main/': ['logined'],
'/main/index.html': ['logined'],
'/main/base.ui': ['logined'],
'/base.ui': ['logined'],
'/login.ui': ['any'],
'/login.dspy': ['any'],
},
# ----------------------------------------------------------
# 客户管理模块 - 销售+财务+管理员
# ----------------------------------------------------------

View File

@ -1,29 +1,100 @@
{
"type": "Page",
"title": "CRM Login",
"style": {"background": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"},
"content": {
"type": "VBox",
"align": "center",
"justify": "center",
"style": {"minHeight": "100vh", "padding": "20px"},
"children": [
{
"type": "Card",
"style": {"width": "400px", "maxWidth": "95vw", "borderRadius": "16px", "boxShadow": "0 8px 32px rgba(0,0,0,0.15)", "padding": "32px"},
"content": {
"type": "VBox",
"gap": 16,
"children": [
{"type": "Text", "content": "CRM System", "style": {"fontSize": "24px", "fontWeight": "bold", "textAlign": "center", "color": "#333"}},
{"type": "Text", "content": "Please login to continue", "style": {"fontSize": "14px", "textAlign": "center", "color": "#666"}},
{"type": "TextField", "id": "username", "placeholder": "Username", "prefixIcon": "person", "style": {"width": "100%"}},
{"type": "TextField", "id": "password", "placeholder": "Password", "prefixIcon": "lock", "password": true, "style": {"width": "100%"}},
{"type": "Button", "id": "loginBtn", "text": "Login", "variant": "primary", "fullWidth": true, "size": "large"},
{"type": "Text", "id": "errorMsg", "content": "", "style": {"color": "#d32f2f", "fontSize": "12px", "textAlign": "center"}}
"id": "login_page",
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100vh",
"style": {
"background": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
"alignItems": "center",
"justifyContent": "center"
}
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"width": "400px",
"style": {
"background": "white",
"borderRadius": "12px",
"boxShadow": "0 8px 32px rgba(0,0,0,0.15)",
"padding": "40px 32px"
}
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "CRM系统登录",
"style": {
"fontSize": "24px",
"fontWeight": "bold",
"textAlign": "center",
"marginBottom": "24px",
"color": "#333"
}
}
},
{
"widgettype": "Form",
"id": "login_form",
"options": {
"url": "{{entire_url('api/login.dspy')}}",
"method": "POST",
"fields": [
{
"name": "username",
"label": "用户名",
"uitype": "str",
"required": true,
"placeholder": "请输入用户名"
},
{
"name": "password",
"label": "密码",
"uitype": "password",
"required": true,
"placeholder": "请输入密码"
}
],
"submitLabel": "登录",
"submitVariant": "primary",
"submitFullWidth": true
}
},
{
"widgettype": "HBox",
"options": {
"style": {
"justifyContent": "center",
"marginTop": "16px",
"gap": "16px"
}
},
"subwidgets": [
{
"widgettype": "Button",
"id": "btn_register",
"options": {
"label": "注册账号",
"variant": "text",
"url": "/rbac/user/register.ui"
}
}
]
}
]
}
],
"binds": [
{
"wid": "login_form",
"event": "submit",
"actiontype": "formsubmit",
"callback": {
"success": "navigate('/index.ui')"
}
]
}
}
}
]
}