"""Users cannot self-grant project access through the permission administration APIs.""" import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import Session from sqlalchemy.pool import StaticPool from app.api.admin import router from app.core.auth import get_current_user from app.core.database import get_db from app.models.base import Base from app.models.models import SysMenu, SysPost, SysRole, SysRoleMenu, SysUser, SysUserRole @pytest.fixture def env(): engine = create_engine('sqlite://', connect_args={'check_same_thread': False}, poolclass=StaticPool) Base.metadata.create_all(engine) db = Session(engine) user = dict(user_id='u', role='admin', tenant_no='t') db.add_all([ SysUser(id='u', tenant_no='t', user_no='member', nick_name='Member'), SysUser(id='foreign-user', tenant_no='foreign', user_no='foreign', nick_name='Foreign'), SysRole(id='role', tenant_no='t', role_name='Member', data_scope='4'), SysRole(id='foreign-role', tenant_no='foreign', role_name='Foreign'), SysUserRole(id='ur', tenant_no='t', user_id='u', role_id='role'), SysPost(id='post', tenant_no='t', post_name='Post', post_code='POST'), SysPost(id='foreign-post', tenant_no='foreign', post_name='Foreign', post_code='FOREIGN'), SysMenu(id='root', tenant_no='t', parent_id='0', menu_name='Web'), SysMenu(id='project', tenant_no='t', parent_id='root', menu_name='项目', perms='menu:project'), SysMenu(id='all', tenant_no='t', parent_id='project', menu_name='全部项目', menu_type='F', perms='project:project:all'), SysMenu(id='foreign-menu', tenant_no='foreign', parent_id='0', menu_name='Foreign'), ]) db.commit() app = FastAPI() app.include_router(router) app.dependency_overrides[get_current_user] = lambda: user app.dependency_overrides[get_db] = lambda: db with TestClient(app) as client: yield db, user, client db.close() engine.dispose() def grant(db, code): db.add_all([SysMenu(id=code, tenant_no='t', menu_name=code, perms=code), SysRoleMenu(id=code, tenant_no='t', role_id='role', menu_id=code)]) db.commit() @pytest.mark.parametrize('method,path,body', [ ('PUT', '/role-management/role/menus', {'menuIds': ['all']}), ('POST', '/role-management', {'roleName': '超级管理员'}), ('PUT', '/role-management/role', {'roleName': '超级管理员'}), ('DELETE', '/role-management/role', None), ('PUT', '/role-management/role/data-scope', {'dataScope': '1'}), ('POST', '/users', {'userNo': 'attacker', 'nickName': 'Attacker', 'password': 'TestOnly123!', 'roleIds': ['role']}), ('PUT', '/users/u', {'roleIds': ['foreign-role']}), ('PUT', '/users/u', {'postIds': ['post']}), ('POST', '/users/u/reset-password', None), ('DELETE', '/users/u', None), ('PUT', '/posts/post/roles', {'roleIds': ['role']}), ('PUT', '/posts/post/users', {'userIds': ['u']}), ('PUT', '/posts/post', {'status': '1'}), ('POST', '/menus', {'menuName': 'Grant', 'perms': 'project:project:all'}), ('PUT', '/menus/project', {'perms': 'project:project:all'}), ('DELETE', '/menus/all', None), ]) def test_business_admin_cannot_mutate_permission_graph(env, method, path, body): _, _, client = env result = client.request(method, '/web/admin' + path, json=body) assert result.status_code == 403 def test_role_manager_grants_leaf_and_ancestors_only(env): db, _, client = env grant(db, 'system:role:list') result = client.put('/web/admin/role-management/role/menus', json={'menuIds': ['all', 'system:role:list']}) assert result.json()['code'] == 200 rows = db.query(SysRoleMenu).filter_by(role_id='role', tenant_no='t', del_flag=0).all() assert {r.menu_id for r in rows} == {'all', 'project', 'root', 'system:role:list'} # Same save is idempotent and does not create duplicate active grants. assert client.put('/web/admin/role-management/role/menus', json={'menuIds': ['all', 'system:role:list']}).json()['code'] == 200 assert db.query(SysRoleMenu).filter_by(role_id='role', tenant_no='t', del_flag=0).count() == 4 @pytest.mark.parametrize('super_admin', [False, True]) @pytest.mark.parametrize('path,body', [ ('/role-management/foreign-role/menus', {'menuIds': ['all']}), ('/role-management/role/menus', {'menuIds': ['all', 'foreign-menu']}), ('/role-management/foreign-role', {'roleName': 'Changed'}), ('/role-management/foreign-role/data-scope', {'dataScope': '1'}), ('/menus/foreign-menu', {'perms': 'project:project:all'}), ('/menus/project', {'parentId': 'foreign-menu'}), ('/posts/foreign-post', {'status': '1'}), ('/users/foreign-user', {'roleIds': ['role']}), ('/users/u', {'roleIds': ['foreign-role']}), ('/users/u', {'postIds': ['foreign-post']}), ]) def test_permission_admin_cannot_write_foreign_targets_or_links(env, path, body, super_admin): db, user, client = env for code in ('system:role:list', 'system:menu:list', 'system:post:list', 'system:user:list'): grant(db, code) if super_admin: user['role'] = 'super_admin' before = {r.menu_id for r in db.query(SysRoleMenu).filter_by(role_id='role', del_flag=0)} result = client.put('/web/admin' + path, json=body) assert result.json().get('code', result.status_code) in (400, 403, 404) db.expire_all() assert {r.menu_id for r in db.query(SysRoleMenu).filter_by(role_id='role', del_flag=0)} == before assert db.get(SysRole, 'foreign-role').role_name == 'Foreign' assert db.get(SysUser, 'foreign-user').tenant_no == 'foreign' @pytest.mark.parametrize('parent', ['missing', 'all']) def test_role_grant_rejects_broken_or_cyclic_ancestry_atomically(env, parent): db, user, client = env user['role'] = 'super_admin' db.get(SysMenu, 'project').parent_id = parent db.commit() result = client.put('/web/admin/role-management/role/menus', json={'menuIds': ['all']}) assert result.json().get('code', result.status_code) == 400 assert db.query(SysRoleMenu).count() == 0 @pytest.mark.parametrize('code,path,body', [ ('system:role:list', '/role-management/role', {'remark': 'authorized'}), ('system:role:list', '/role-management/role/data-scope', {'dataScope': '4'}), ('system:user:list', '/users/u', {'roleIds': ['role'], 'postIds': ['post']}), ('system:post:list', '/posts/post/roles', {'roleIds': ['role']}), ('system:post:list', '/posts/post/users', {'userIds': ['u']}), ('system:menu:list', '/menus/all', {'menuName': '全部项目'}), ]) def test_authorized_tenant_administrators_can_manage_their_module(env, code, path, body): db, _, client = env grant(db, code) assert client.put('/web/admin' + path, json=body).json()['code'] == 200 def test_web_and_mobile_ancestor_roots_remain_separate(env): db, user, client = env user['role'] = 'super_admin' db.add_all([ SysMenu(id='mobile-root', tenant_no='t', parent_id='0', menu_name='Mobile'), SysMenu(id='mobile-leaf', tenant_no='t', parent_id='mobile-root', menu_name='Mobile business'), ]) db.commit() assert client.put('/web/admin/role-management/role/menus', json={'menuIds': ['all', 'mobile-leaf']}).json()['code'] == 200 assert {r.menu_id for r in db.query(SysRoleMenu).filter_by(role_id='role', del_flag=0)} == {'all', 'project', 'root', 'mobile-leaf', 'mobile-root'} @pytest.mark.parametrize('method,path,body', [ ('POST', '/qc-material-amount-auth/users', {'userId': 'u'}), ('DELETE', '/qc-material-amount-auth/users/u', None), ('POST', '/qc-material-amount-auth/users/batch-revoke', {'userIds': ['u']}), ]) def test_business_admin_cannot_self_bind_specialized_authorization_roles(env, method, path, body): from app.api.qc import router as qc_router _, _, client = env client.app.include_router(qc_router) assert client.request(method, '/web/admin' + path, json=body).status_code == 403 def test_specialized_authorization_manager_can_grant_and_revoke(env): from app.api.qc import router as qc_router from app.core.qc_material_amount_auth_constants import ( QC_MATERIAL_AMOUNT_AUTH_LIST_PERM, QC_MATERIAL_AMOUNT_VIEW_ROLE_ID, QC_MATERIAL_AMOUNT_VIEW_ROLE_NAME, QC_MATERIAL_AMOUNT_AUTH_DEPT_NAME, ) from app.models.models import SysDept db, _, client = env client.app.include_router(qc_router) grant(db, QC_MATERIAL_AMOUNT_AUTH_LIST_PERM) db.add_all([ SysDept(id='qc-dept', tenant_no='t', dept_name=QC_MATERIAL_AMOUNT_AUTH_DEPT_NAME), SysRole(id=QC_MATERIAL_AMOUNT_VIEW_ROLE_ID, tenant_no='t', role_name=QC_MATERIAL_AMOUNT_VIEW_ROLE_NAME), ]) db.get(SysUser, 'u').dept_id = 'qc-dept' db.commit() assert client.post('/web/admin/qc-material-amount-auth/users', json={'userId': 'u'}).json()['code'] == 200 assert client.delete('/web/admin/qc-material-amount-auth/users/u').json()['code'] == 200