| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784 |
- import asyncio
- import pytest
- from fastapi import HTTPException
- from sqlalchemy import create_engine
- from sqlalchemy.orm import sessionmaker
- from app.core.mobile_menu_guard import (
- require_market_menu_code,
- require_market_menu_permission,
- user_has_tenant_menu_perm,
- )
- from app.models.base import Base
- from app.models.models import (
- BizPresignOrder,
- BizPresignOrderSampleType,
- BizPresignTransfer,
- SysMenu,
- SysDept,
- SysPost,
- SysPostRole,
- SysRole,
- SysRoleDataScopeDetail,
- SysRoleMenu,
- SysUser,
- SysUserPost,
- SysUserRole,
- )
- def _session():
- engine = create_engine("sqlite:///:memory:")
- Base.metadata.create_all(
- engine,
- tables=[
- SysMenu.__table__,
- SysDept.__table__,
- SysPost.__table__,
- SysPostRole.__table__,
- SysRole.__table__,
- SysRoleDataScopeDetail.__table__,
- SysRoleMenu.__table__,
- SysUser.__table__,
- SysUserPost.__table__,
- SysUserRole.__table__,
- BizPresignOrder.__table__,
- BizPresignOrderSampleType.__table__,
- BizPresignTransfer.__table__,
- ],
- )
- return sessionmaker(bind=engine)()
- def _add_role_menu(
- db,
- *,
- tenant_no,
- role_id,
- menu_id,
- perm_code,
- role_name="市场角色",
- data_scope="1",
- ):
- db.add(
- SysRole(
- id=role_id,
- role_name=role_name,
- data_scope=data_scope,
- status="1",
- tenant_no=tenant_no,
- create_dept_id="",
- del_flag=0,
- )
- )
- db.add(
- SysMenu(
- id=menu_id,
- menu_name=perm_code,
- parent_id="root",
- menu_type="C",
- path=f"/{menu_id}",
- perms=perm_code,
- visible="1",
- status="1",
- tenant_no=tenant_no,
- create_dept_id="",
- del_flag=0,
- )
- )
- db.add(
- SysRoleMenu(
- id=f"rm-{role_id}-{menu_id}",
- role_id=role_id,
- menu_id=menu_id,
- tenant_no=tenant_no,
- create_dept_id="",
- del_flag=0,
- )
- )
- def test_specialist_permission_does_not_grant_manager_access():
- assert not require_market_menu_permission(
- user_id="u1",
- tenant_no="t1",
- required="mini:market:manager:presign",
- granted={"mini:market:specialist:presign"},
- )
- def test_tenant_menu_permission_does_not_fall_back_to_default_tenant():
- db = _session()
- _add_role_menu(
- db,
- tenant_no="tenant-a",
- role_id="role-a",
- menu_id="menu-a",
- perm_code="mini:market:manager:presign",
- )
- _add_role_menu(
- db,
- tenant_no="tenant-b",
- role_id="role-b",
- menu_id="menu-b",
- perm_code="mini:market:specialist:presign",
- )
- db.add_all(
- [
- SysUserRole(
- id="ur-a",
- user_id="u1",
- role_id="role-a",
- tenant_no="tenant-a",
- create_dept_id="",
- del_flag=0,
- ),
- SysUserRole(
- id="ur-b",
- user_id="u1",
- role_id="role-b",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- ]
- )
- db.commit()
- assert user_has_tenant_menu_perm(
- db,
- user_id="u1",
- tenant_no="tenant-b",
- perm_code="mini:market:specialist:presign",
- )
- assert not user_has_tenant_menu_perm(
- db,
- user_id="u1",
- tenant_no="tenant-b",
- perm_code="mini:market:manager:presign",
- )
- def test_tenant_menu_permission_includes_post_inherited_roles():
- db = _session()
- _add_role_menu(
- db,
- tenant_no="tenant-b",
- role_id="role-b",
- menu_id="menu-b",
- perm_code="mini:market:specialist:presign",
- )
- db.add_all(
- [
- SysPost(
- id="post-b",
- post_name="市场专员",
- post_code="specialist",
- status="1",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysUserPost(
- id="up-b",
- user_id="u1",
- post_id="post-b",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysPostRole(
- id="pr-b",
- post_id="post-b",
- role_id="role-b",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- ]
- )
- db.commit()
- assert user_has_tenant_menu_perm(
- db,
- user_id="u1",
- tenant_no="tenant-b",
- perm_code="mini:market:specialist:presign",
- )
- def test_strict_guard_rejects_missing_tenant_identity():
- dependency = require_market_menu_code("mini:market:specialist:presign")
- with pytest.raises(HTTPException) as exc_info:
- asyncio.run(dependency(user={"user_id": "u1", "tenant_no": ""}, db=None))
- assert exc_info.value.status_code == 403
- assert exc_info.value.detail == "用户租户或身份信息缺失"
- def test_strict_guard_does_not_trust_cross_tenant_super_admin_role():
- db = _session()
- db.add_all(
- [
- SysRole(
- id="tenant-b-role",
- role_name="市场专员",
- status="1",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysUserRole(
- id="tenant-b-user-role",
- user_id="u1",
- role_id="tenant-b-role",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- ]
- )
- db.commit()
- dependency = require_market_menu_code("mini:market:specialist:presign")
- with pytest.raises(HTTPException) as exc_info:
- asyncio.run(
- dependency(
- user={
- "user_id": "u1",
- "tenant_no": "tenant-b",
- "role": "super_admin",
- },
- db=db,
- )
- )
- assert exc_info.value.status_code == 403
- assert exc_info.value.detail == "无移动端菜单权限"
- def test_strict_guard_uses_current_tenant_super_admin_role():
- db = _session()
- db.add_all(
- [
- SysRole(
- id="tenant-b-super-role",
- role_name="超级管理员",
- status="1",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysUserRole(
- id="tenant-b-super-user-role",
- user_id="u1",
- role_id="tenant-b-super-role",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- ]
- )
- db.commit()
- dependency = require_market_menu_code("mini:market:specialist:presign")
- user = {"user_id": "u1", "tenant_no": "tenant-b", "role": "admin"}
- result = asyncio.run(dependency(user=user, db=db))
- assert result is user
- def _add_auth_user(db, *, tenant_no="tenant-b"):
- db.add(
- SysUser(
- id="u1",
- user_no="u1",
- nick_name="租户用户",
- password="unused",
- status="1",
- tenant_no=tenant_no,
- create_dept_id="dept-b",
- del_flag=0,
- )
- )
- def _add_cross_tenant_roles(db):
- from app.core.config import settings
- default_tenant = settings.DEFAULT_TENANT_NO
- _add_role_menu(
- db,
- tenant_no=default_tenant,
- role_id="default-super",
- menu_id="default-menu",
- perm_code="default:anything",
- role_name="超级管理员",
- data_scope="1",
- )
- _add_role_menu(
- db,
- tenant_no="tenant-b",
- role_id="tenant-b-specialist",
- menu_id="tenant-b-presign",
- perm_code="mini:market:specialist:presign",
- role_name="市场专员",
- data_scope="4",
- )
- db.add_all(
- [
- SysUserRole(
- id="default-user-role",
- user_id="u1",
- role_id="default-super",
- tenant_no=default_tenant,
- create_dept_id="",
- del_flag=0,
- ),
- SysUserRole(
- id="tenant-b-user-role",
- user_id="u1",
- role_id="tenant-b-specialist",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- ]
- )
- def _add_presign(db, order_id, *, create_by):
- db.add(
- BizPresignOrder(
- id=order_id,
- mother_name=order_id,
- sales_user_id="u1",
- order_status="active",
- tenant_no="tenant-b",
- create_by=create_by,
- create_dept_id="dept-b",
- del_flag=0,
- )
- )
- def _authenticate(db, monkeypatch):
- from app.core import auth
- monkeypatch.setattr(
- auth,
- "decode_token",
- lambda token: {"sub": "u1", "sid": "session-u1"},
- )
- async def valid_session(session_id):
- return session_id == "session-u1"
- async def extend_session(session_id):
- return None
- monkeypatch.setattr(auth, "is_session_valid", valid_session)
- monkeypatch.setattr(auth, "extend_session", extend_session)
- return asyncio.run(auth.get_current_user_by_token("token", db))
- def test_authenticated_tenant_role_drives_menu_and_presign_data_scope(
- monkeypatch,
- ):
- from app.services.market_field_scope import MarketFieldContext
- from app.services.market_presign import list_presign_orders
- db = _session()
- _add_auth_user(db)
- _add_cross_tenant_roles(db)
- _add_presign(db, "own", create_by="u1")
- _add_presign(db, "field-only", create_by="other")
- db.commit()
- current_user = _authenticate(db, monkeypatch)
- dependency = require_market_menu_code("mini:market:specialist:presign")
- guarded_user = asyncio.run(dependency(user=current_user, db=db))
- rows, total = list_presign_orders(
- db,
- ctx=MarketFieldContext("tenant-b", "u1", "specialist"),
- user_info=guarded_user,
- )
- assert current_user["tenant_no"] == "tenant-b"
- assert current_user["role"] == "admin"
- assert current_user["role_names"] == ["市场专员"]
- assert total == 1
- assert [row["id"] for row in rows] == ["own"]
- def test_authenticated_current_tenant_super_admin_remains_unrestricted(
- monkeypatch,
- ):
- from app.services.market_field_scope import MarketFieldContext
- from app.services.market_presign import list_presign_orders
- db = _session()
- _add_auth_user(db)
- db.add_all(
- [
- SysRole(
- id="tenant-b-super",
- role_name="超级管理员",
- data_scope="1",
- status="1",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysUserRole(
- id="tenant-b-super-user-role",
- user_id="u1",
- role_id="tenant-b-super",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- ]
- )
- _add_presign(db, "own", create_by="u1")
- _add_presign(db, "field-only", create_by="other")
- db.commit()
- current_user = _authenticate(db, monkeypatch)
- dependency = require_market_menu_code("mini:market:specialist:presign")
- guarded_user = asyncio.run(dependency(user=current_user, db=db))
- rows, total = list_presign_orders(
- db,
- ctx=MarketFieldContext("tenant-b", "u1", "specialist"),
- user_info=guarded_user,
- )
- assert current_user["role"] == "super_admin"
- assert current_user["role_names"] == ["超级管理员"]
- assert total == 2
- assert {row["id"] for row in rows} == {"own", "field-only"}
- def test_token_issuance_uses_the_users_current_tenant_roles(monkeypatch):
- from app.services import issue_user_tokens
- db = _session()
- _add_auth_user(db)
- _add_cross_tenant_roles(db)
- db.commit()
- async def no_op(**kwargs):
- return None
- monkeypatch.setattr(issue_user_tokens, "store_session", no_op)
- monkeypatch.setattr(issue_user_tokens, "store_refresh_token", no_op)
- issued = asyncio.run(
- issue_user_tokens.issue_tokens_for_user(
- db, db.query(SysUser).filter_by(id="u1").one()
- )
- )
- assert issued["userInfo"]["role"] == "admin"
- assert issued["userInfo"]["roleNames"] == ["市场专员"]
- def test_public_auth_entrypoints_pass_the_users_tenant_to_role_queries(
- monkeypatch,
- ):
- from app.api import public
- from app.schemas.auth import ProfileUpdateRequest
- db = _session()
- _add_auth_user(db)
- db.commit()
- user = db.query(SysUser).filter_by(id="u1").one()
- calls = {"active": [], "role": [], "names": [], "menus": []}
- def has_active_roles(db_arg, user_id, tenant_no=None):
- calls["active"].append((user_id, tenant_no))
- return True
- def resolve_role(db_arg, user_id, tenant_no=None):
- calls["role"].append((user_id, tenant_no))
- return "admin"
- def role_names(db_arg, user_id, tenant_no=None):
- calls["names"].append((user_id, tenant_no))
- return ["市场专员"]
- def menu_codes(db_arg, user_id, tenant_no=None):
- calls["menus"].append((user_id, tenant_no))
- return {"mini:market:specialist:presign"}
- async def no_op(*args, **kwargs):
- return None
- async def valid(*args, **kwargs):
- return True
- current_user = {
- "user_id": "u1",
- "tenant_no": "tenant-b",
- "role": "admin",
- }
- monkeypatch.setattr(public.login_crypto, "ensure_initialized", lambda: None)
- monkeypatch.setattr(
- public.login_crypto,
- "extract_login_credentials",
- lambda body: ("u1", "password", None),
- )
- monkeypatch.setattr(public.user_crud, "get_by_user_no", lambda db_arg, no: user)
- monkeypatch.setattr(public, "verify_password", lambda raw, hashed: True)
- monkeypatch.setattr(public, "user_has_active_roles", has_active_roles)
- monkeypatch.setattr(public, "resolve_web_role_for_user", resolve_role)
- monkeypatch.setattr(public, "get_user_role_names", role_names)
- monkeypatch.setattr(public, "iter_user_menu_perm_codes", menu_codes)
- monkeypatch.setattr(public, "create_access_token", lambda **kwargs: "access")
- monkeypatch.setattr(public, "create_refresh_token", lambda **kwargs: "refresh")
- monkeypatch.setattr(public, "store_session", no_op)
- monkeypatch.setattr(public, "store_refresh_token", no_op)
- monkeypatch.setattr(public, "revoke_refresh_token", no_op)
- monkeypatch.setattr(public, "extend_session", no_op)
- monkeypatch.setattr(public, "is_session_valid", valid)
- monkeypatch.setattr(public, "is_refresh_token_valid", valid)
- monkeypatch.setattr(
- public,
- "decode_token",
- lambda token: {
- "type": "refresh",
- "sid": "session-u1",
- "jti": "refresh-u1",
- "sub": "u1",
- },
- )
- monkeypatch.setattr(public, "update_user", lambda *args, **kwargs: None)
- async def resolve_current(request, db_arg):
- return current_user
- async def current_by_token(token, db):
- return current_user
- monkeypatch.setattr(public, "_resolve_current_user_from_request", resolve_current)
- monkeypatch.setattr(public, "get_current_user_by_token", current_by_token)
- class RequestStub:
- headers = {"Authorization": "Bearer token"}
- asyncio.run(public.web_login(request=None, body={}, db=db))
- asyncio.run(public.refresh_token({"refreshToken": "refresh"}, db=db))
- asyncio.run(
- public.update_current_user_profile(
- request=RequestStub(),
- body=ProfileUpdateRequest(),
- db=db,
- )
- )
- asyncio.run(public.get_web_auth_menu_codes(request=RequestStub(), db=db))
- assert calls == {
- "active": [("u1", "tenant-b")],
- "role": [("u1", "tenant-b")] * 3,
- "names": [("u1", "tenant-b")] * 2,
- "menus": [("u1", "tenant-b")],
- }
- def test_org_labels_are_tenant_scoped_in_auth_profile_and_token_payloads(
- monkeypatch,
- ):
- from app.api import public
- from app.core.config import settings
- from app.crud.user import list_post_ids_for_user
- from app.schemas.auth import ProfileUpdateRequest
- from app.services import issue_user_tokens
- db = _session()
- _add_auth_user(db)
- _add_role_menu(
- db,
- tenant_no="tenant-b",
- role_id="tenant-b-role",
- menu_id="tenant-b-menu",
- perm_code="mini:market:specialist:presign",
- role_name="市场专员",
- data_scope="4",
- )
- user = db.query(SysUser).filter_by(id="u1").one()
- user.dept_id = "default-dept"
- db.add_all(
- [
- SysUserRole(
- id="tenant-b-role-link",
- user_id="u1",
- role_id="tenant-b-role",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysDept(
- id="default-dept",
- dept_name="默认租户部门",
- tenant_no=settings.DEFAULT_TENANT_NO,
- create_dept_id="",
- del_flag=0,
- ),
- SysDept(
- id="tenant-b-dept",
- dept_name="租户B部门",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysDept(
- id="tenant-b-deleted-dept",
- dept_name="租户B已删除部门",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=1,
- ),
- SysPost(
- id="default-post",
- post_name="默认租户职位",
- post_code="default-post",
- status="1",
- tenant_no=settings.DEFAULT_TENANT_NO,
- create_dept_id="",
- del_flag=0,
- ),
- SysPost(
- id="tenant-b-post",
- post_name="租户B有效职位",
- post_code="tenant-b-post",
- status="1",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysPost(
- id="tenant-b-disabled-post",
- post_name="租户B停用职位",
- post_code="tenant-b-disabled-post",
- status="0",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysPost(
- id="tenant-b-deleted-post",
- post_name="租户B已删除职位",
- post_code="tenant-b-deleted-post",
- status="1",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=1,
- ),
- SysPost(
- id="tenant-b-unbound-post",
- post_name="租户B已解绑职位",
- post_code="tenant-b-unbound-post",
- status="1",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysUserPost(
- id="default-user-post",
- user_id="u1",
- post_id="default-post",
- tenant_no=settings.DEFAULT_TENANT_NO,
- create_dept_id="",
- del_flag=0,
- ),
- SysUserPost(
- id="tenant-b-user-post",
- user_id="u1",
- post_id="tenant-b-post",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysUserPost(
- id="tenant-b-disabled-user-post",
- user_id="u1",
- post_id="tenant-b-disabled-post",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysUserPost(
- id="tenant-b-deleted-user-post",
- user_id="u1",
- post_id="tenant-b-deleted-post",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=0,
- ),
- SysUserPost(
- id="tenant-b-unbound-user-post",
- user_id="u1",
- post_id="tenant-b-unbound-post",
- tenant_no="tenant-b",
- create_dept_id="",
- del_flag=1,
- ),
- ]
- )
- db.commit()
- dept_name, post_names = issue_user_tokens.resolve_user_org_labels(db, user)
- assert dept_name is None
- assert post_names == ["租户B有效职位"]
- assert set(list_post_ids_for_user(db, "u1")) == {
- "default-post",
- "tenant-b-post",
- "tenant-b-disabled-post",
- "tenant-b-deleted-post",
- }
- assert set(list_post_ids_for_user(db, "u1", tenant_no="tenant-b")) == {
- "tenant-b-post",
- "tenant-b-disabled-post",
- "tenant-b-deleted-post",
- }
- user.dept_id = "tenant-b-deleted-dept"
- db.commit()
- dept_name, post_names = issue_user_tokens.resolve_user_org_labels(db, user)
- assert dept_name is None
- assert post_names == ["租户B有效职位"]
- user.dept_id = "tenant-b-dept"
- db.commit()
- authenticated = _authenticate(db, monkeypatch)
- assert authenticated["dept_name"] == "租户B部门"
- assert authenticated["post_names"] == ["租户B有效职位"]
- async def no_op(*args, **kwargs):
- return None
- monkeypatch.setattr(issue_user_tokens, "store_session", no_op)
- monkeypatch.setattr(issue_user_tokens, "store_refresh_token", no_op)
- issued = asyncio.run(issue_user_tokens.issue_tokens_for_user(db, user))
- assert issued["userInfo"]["deptName"] == "租户B部门"
- assert issued["userInfo"]["postNames"] == ["租户B有效职位"]
- current_user = {"user_id": "u1", "tenant_no": "tenant-b"}
- async def resolve_current(request, db_arg):
- return current_user
- monkeypatch.setattr(public, "_resolve_current_user_from_request", resolve_current)
- monkeypatch.setattr(public, "update_user", lambda *args, **kwargs: None)
- profiled = asyncio.run(
- public.update_current_user_profile(
- request=None,
- body=ProfileUpdateRequest(),
- db=db,
- )
- )
- assert profiled["data"]["deptName"] == "租户B部门"
- assert profiled["data"]["postNames"] == ["租户B有效职位"]
|