From f932bfb088477b69f5987ec4a011f112468d5fc7 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 3 May 2026 14:26:27 +0800 Subject: [PATCH] bugfix --- app/init_permissions.py | 160 ++++++++++++++++++++++++++++++++++++---- app/perm_config.py | 33 ++++++++- wwwroot/login.ui | 121 +++++++++++++++++++++++------- 3 files changed, 273 insertions(+), 41 deletions(-) diff --git a/app/init_permissions.py b/app/init_permissions.py index fdee831..af59415 100644 --- a/app/init_permissions.py +++ b/app/init_permissions.py @@ -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__)) diff --git a/app/perm_config.py b/app/perm_config.py index 27e52d2..6b06c77 100644 --- a/app/perm_config.py +++ b/app/perm_config.py @@ -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'], + }, + # ---------------------------------------------------------- # 客户管理模块 - 销售+财务+管理员 # ---------------------------------------------------------- diff --git a/wwwroot/login.ui b/wwwroot/login.ui index 7a86b34..df6031c 100644 --- a/wwwroot/login.ui +++ b/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')" } - ] - } -} \ No newline at end of file + } + ] +}