Jelajahi Sumber

fix: scope user organization labels to the current tenant.

zlwu 3 minggu lalu
induk
melakukan
81bda0cb79

+ 10 - 4
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()
     ]
 
 

+ 15 - 3
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 = {

+ 196 - 0
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有效职位"]