bugfix
This commit is contained in:
parent
de8ce23fcb
commit
f932bfb088
@ -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: 创建约定角色(固定ID,rbac/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__))
|
||||
|
||||
@ -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'],
|
||||
},
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 客户管理模块 - 销售+财务+管理员
|
||||
# ----------------------------------------------------------
|
||||
|
||||
121
wwwroot/login.ui
121
wwwroot/login.ui
@ -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')"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user