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 os
import sys import sys
import asyncio import asyncio
import fnmatch
from appPublic.uniqueID import getID from appPublic.uniqueID import getID
from appPublic.log import debug, info from appPublic.log import debug, info
from appPublic.jsonConfig import getConfig
from sqlor.dbpools import DBPools 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'): async def ensure_permission(sor, path, name='', permtype='page'):
"""Ensure a permission exists in the database.""" """Ensure a permission exists in the database."""
recs = await sor.R('permission', {'path': path}) 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=''): 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}) recs = await sor.R('role', {'id': roleid})
if recs: if recs:
return recs[0].id return recs[0].id
# Create new role
await sor.C('role', { await sor.C('role', {
'id': roleid, 'id': roleid,
'orgtypeid': '*', 'orgtypeid': '*',
@ -62,8 +152,9 @@ async def init_permissions_from_config(dbname, config_module=None):
CRM是单业主机构系统所有角色属于同一业主机构 CRM是单业主机构系统所有角色属于同一业主机构
流程 流程
1. 创建约定角色any/logined/anonymous和定义的角色 1. 创建约定角色any/logined/anonymous和定义的角色
2. 注册 PERMISSION_MATRIX 中的路径权限 2. 扫描 wwwroot 目录展开 ** 通配符为实际文件路径
3. 注册 CRUD 路径并授权 3. 注册权限并授权
4. 注册 CRUD 路径并授权
""" """
if config_module is None: if config_module is None:
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) 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 PERMISSION_MATRIX = config_module.PERMISSION_MATRIX
CRUD_TABLES = config_module.CRUD_TABLES 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() db = DBPools()
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
# Step 1: 创建约定角色固定IDrbac/userperm.py硬编码检查 # Step 1: 创建约定角色固定IDrbac/userperm.py硬编码检查
info('Creating convention roles...') info('Creating convention roles...')
role_ids = {} role_ids = {}
# 约定角色必须用固定ID
for fixed_id in ['any', 'logined', 'anonymous']: for fixed_id in ['any', 'logined', 'anonymous']:
role_ids[fixed_id] = await ensure_role(sor, fixed_id, fixed_id) 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', '') sor, role['id'], role['name'], role.get('desc', '')
) )
# Step 3: 注册权限矩阵中的路径并授权 # Step 3: 注册展开后的路径权限并授权
info('Registering permissions from matrix...') info('Registering expanded permissions...')
perm_count = 0 perm_count = 0
for module, paths in PERMISSION_MATRIX.items(): for path, roles in role_paths.items():
for path_pattern, role_list in paths.items(): # Register both /xxx and /main/xxx variants
permid = await ensure_permission(sor, path_pattern, # The auth middleware passes request.path (e.g. /main/customer_management/xxx.ui)
name=f'{module}: {path_pattern}', permtype='module') # 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: if role_name in role_ids:
await grant_permission(sor, role_ids[role_name], permid) await grant_permission(sor, role_ids[role_name], permid)
perm_count += 1 perm_count += 1
@ -114,7 +218,7 @@ async def init_permissions_from_config(dbname, config_module=None):
permid = await ensure_permission(sor, crud_path, permid = await ensure_permission(sor, crud_path,
name=f'{module}/{table} CRUD', permtype='crud') name=f'{module}/{table} CRUD', permtype='crud')
# 根据模块权限矩阵授予权限 # Grant to roles that have module-level access
if module in PERMISSION_MATRIX: if module in PERMISSION_MATRIX:
for path_pattern, role_list in PERMISSION_MATRIX[module].items(): for path_pattern, role_list in PERMISSION_MATRIX[module].items():
if path_pattern.startswith(f'/{module}'): if path_pattern.startswith(f'/{module}'):
@ -124,12 +228,42 @@ async def init_permissions_from_config(dbname, config_module=None):
perm_count += 1 perm_count += 1
info(f'Permission initialization complete: {perm_count} grants created') 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(): def main():
"""Run permission initialization.""" """Run permission initialization."""
import json import json
from appPublic.jsonConfig import getConfig
from appPublic.dictObject import DictObject from appPublic.dictObject import DictObject
config_path = os.path.dirname(os.path.dirname(__file__)) config_path = os.path.dirname(os.path.dirname(__file__))

View File

@ -25,7 +25,9 @@ ROLES = [
# 财务部 # 财务部
{'id': 'finance_admin', 'name': '财务管理员', 'desc': '所有财务操作、报表'}, {'id': 'finance_admin', 'name': '财务管理员', 'desc': '所有财务操作、报表'},
{'id': 'finance_clerk', 'name': '财务出纳', 'desc': '收款登记、付款处理'}, {'id': 'finance_clerk', 'name': '财务出纳', 'desc': '收款登记、付款处理'},
# 管理员 # 管理员(兼容已有数据库中的角色名)
{'id': 'admin', 'name': '管理员', 'desc': '全部权限'},
{'id': 'superuser', 'name': '超级用户', 'desc': '全部权限'},
{'id': 'admin_superuser', 'name': '超级用户', 'desc': '全部权限,初始化用'}, {'id': 'admin_superuser', 'name': '超级用户', 'desc': '全部权限,初始化用'},
] ]
@ -35,13 +37,38 @@ ROLES = [
# ============================================================ # ============================================================
PERMISSION_MATRIX = { 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.ui': ['any'],
'/main/login.dspy': ['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", "id": "login_page",
"title": "CRM Login", "widgettype": "VBox",
"style": {"background": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"}, "options": {
"content": { "width": "100%",
"type": "VBox", "height": "100vh",
"align": "center", "style": {
"justify": "center", "background": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
"style": {"minHeight": "100vh", "padding": "20px"}, "alignItems": "center",
"children": [ "justifyContent": "center"
{ }
"type": "Card", },
"style": {"width": "400px", "maxWidth": "95vw", "borderRadius": "16px", "boxShadow": "0 8px 32px rgba(0,0,0,0.15)", "padding": "32px"}, "subwidgets": [
"content": { {
"type": "VBox", "widgettype": "VBox",
"gap": 16, "options": {
"children": [ "width": "400px",
{"type": "Text", "content": "CRM System", "style": {"fontSize": "24px", "fontWeight": "bold", "textAlign": "center", "color": "#333"}}, "style": {
{"type": "Text", "content": "Please login to continue", "style": {"fontSize": "14px", "textAlign": "center", "color": "#666"}}, "background": "white",
{"type": "TextField", "id": "username", "placeholder": "Username", "prefixIcon": "person", "style": {"width": "100%"}}, "borderRadius": "12px",
{"type": "TextField", "id": "password", "placeholder": "Password", "prefixIcon": "lock", "password": true, "style": {"width": "100%"}}, "boxShadow": "0 8px 32px rgba(0,0,0,0.15)",
{"type": "Button", "id": "loginBtn", "text": "Login", "variant": "primary", "fullWidth": true, "size": "large"}, "padding": "40px 32px"
{"type": "Text", "id": "errorMsg", "content": "", "style": {"color": "#d32f2f", "fontSize": "12px", "textAlign": "center"}} }
},
"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')"
} }
] }
} ]
} }