diff --git a/backend/app/crud/user.py b/backend/app/crud/user.py index 56475003..e084749d 100644 --- a/backend/app/crud/user.py +++ b/backend/app/crud/user.py @@ -156,13 +156,19 @@ def get_user(db: Session, user_id: str) -> Optional[SysUser]: ) -def list_post_ids_for_user(db: Session, user_id: str) -> List[str]: +def list_post_ids_for_user( + db: Session, user_id: str, tenant_no: Optional[str] = None +) -> List[str]: """用户当前有效职位 ID 列表。""" + query = db.query(SysUserPost).filter( + and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0) + ) + tenant = (tenant_no or "").strip() + if tenant: + query = query.filter(SysUserPost.tenant_no == tenant) return [ r.post_id - for r in db.query(SysUserPost) - .filter(and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0)) - .all() + for r in query.all() ] diff --git a/backend/app/services/issue_user_tokens.py b/backend/app/services/issue_user_tokens.py index 81278323..63e5b96f 100644 --- a/backend/app/services/issue_user_tokens.py +++ b/backend/app/services/issue_user_tokens.py @@ -32,23 +32,35 @@ def _avatar_url_field(user: SysUser) -> Optional[str]: def resolve_user_org_labels(db: Session, user: SysUser) -> Tuple[Optional[str], List[str]]: """解析用户所属部门名称与职位名称列表(保序)。""" + tenant_no = (getattr(user, "tenant_no", None) or "").strip() dept_name: Optional[str] = None dept_id = (getattr(user, "dept_id", None) or "").strip() if dept_id: dept = ( db.query(SysDept) - .filter(SysDept.id == dept_id, SysDept.del_flag == 0) + .filter( + SysDept.id == dept_id, + SysDept.tenant_no == tenant_no, + SysDept.del_flag == 0, + ) .first() ) if dept and (dept.dept_name or "").strip(): dept_name = str(dept.dept_name).strip() post_names: List[str] = [] - post_ids = list_post_ids_for_user(db, user.id) + post_ids = list_post_ids_for_user( + db, user.id, tenant_no=tenant_no + ) if post_ids: posts = ( db.query(SysPost) - .filter(SysPost.id.in_(post_ids), SysPost.del_flag == 0) + .filter( + SysPost.id.in_(post_ids), + SysPost.tenant_no == tenant_no, + SysPost.status == "1", + SysPost.del_flag == 0, + ) .all() ) name_map = { diff --git a/backend/tests/test_market_mobile_permissions.py b/backend/tests/test_market_mobile_permissions.py index bc85e610..9eaa3208 100644 --- a/backend/tests/test_market_mobile_permissions.py +++ b/backend/tests/test_market_mobile_permissions.py @@ -584,3 +584,199 @@ def test_public_auth_entrypoints_pass_the_users_tenant_to_role_queries( "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有效职位"]