记忆召回补一道能力码闸门:归属关系不等于授权
上一版把员工召回改成"按 sys_customer_assignment 归属"时,只看了**归属关系**, 没校验**能力码** —— 这留下一个我自己带出来的口子: - 平台读他人画像/记忆的正式路径(customer_profile_service、public_platform_service) 校验的是 `memory:read:customer`(sys_permission id=9010,data_scope='own_customers') + `customer_ids`; - 而召回是**另一条**把客户记忆送进模型上下文的路径,只按归属放行,等于让 "有归属行但没有该权限"的账号(例如 operator:只有 financial:nl2sql:read + offsite:write)凭空获得读他人客户记忆的能力 —— 它上一版之前是读不到的。 现改为**两个条件都满足才放行**(缺一即失败关闭,并在日志里分开点名"缺能力码" 与"归属未维护",避免运维在错误的表上找问题): ① `memory:read:customer` 在 context.permissions 里; ② 客户落在 context.customer_ids(sys_customer_assignment 生效行)内。 演示账号不受影响:9002(risk_operator)、9020(advisor)、9003(admin) 三个角色都持有该权限码, 真实身份链路复验 9020 仍是"修复前 0 条 → 修复后 2 条"。 测试:tests/unit/core/test_memory_scope.py +3 条(缺能力码读不到/能力码与归属是"与"关系/ 现有用例补上能力码),tests/unit/service/test_agent_governance.py 同步。 `pytest tests/unit tests/contract` → 1448 passed, 2 skipped, 1 failed (唯一失败仍是组员正在改的投顾页面)。
This commit is contained in:
@@ -15,11 +15,15 @@ from app.core.contracts import RequestContext
|
||||
from app.core.memory_scope import (
|
||||
CUSTOMER_ROLES,
|
||||
MAX_RECALL_CUSTOMERS,
|
||||
REQUIRED_EMPLOYEE_PERMISSION,
|
||||
customer_memory_scope,
|
||||
is_customer_identity,
|
||||
memory_customer_in_scope,
|
||||
)
|
||||
|
||||
#: 员工读他人客户记忆的能力码;模拟 `IdentityRepository.load_context` 放进去的权限集合。
|
||||
EMPLOYEE_PERMISSIONS = (REQUIRED_EMPLOYEE_PERMISSION,)
|
||||
|
||||
|
||||
def _context(**overrides: object) -> RequestContext:
|
||||
base: dict[str, object] = {"user_id": "9001", "trace_id": "t"}
|
||||
@@ -37,7 +41,7 @@ def test_customer_reads_only_self() -> None:
|
||||
|
||||
def test_employee_without_assignment_reads_nothing() -> None:
|
||||
"""员工号**不得**被当成客户号 —— 这正是"越权陷阱"与"恒空"的共同来源。"""
|
||||
context = _context(user_id="9020", roles=("advisor",))
|
||||
context = _context(user_id="9020", roles=("advisor",), permissions=EMPLOYEE_PERMISSIONS)
|
||||
|
||||
assert is_customer_identity(context) is False
|
||||
assert customer_memory_scope(context) == ()
|
||||
@@ -46,8 +50,38 @@ def test_employee_without_assignment_reads_nothing() -> None:
|
||||
assert 9020 not in customer_memory_scope(context)
|
||||
|
||||
|
||||
def test_employee_without_the_capability_reads_nothing() -> None:
|
||||
"""**归属关系不等于授权**:有归属行但没有 `memory:read:customer` 能力码,仍读不到。
|
||||
|
||||
这条守的是"召回绕开 RBAC"的口子 —— `operator` 这类只有
|
||||
`financial:nl2sql:read` + `offsite:write` 的账号若有归属行,不能因此获得读他人记忆的能力。
|
||||
"""
|
||||
context = _context(
|
||||
user_id="9005", roles=("operator",), customer_ids=("9001",), permissions=("offsite:write",),
|
||||
)
|
||||
|
||||
assert customer_memory_scope(context) == ()
|
||||
|
||||
|
||||
def test_employee_needs_both_capability_and_assignment() -> None:
|
||||
"""能力码与归属是**与**关系:缺任一条都读不到,两条齐了才给出范围。"""
|
||||
only_capability = _context(
|
||||
user_id="9020", roles=("advisor",), permissions=EMPLOYEE_PERMISSIONS,
|
||||
)
|
||||
both = _context(
|
||||
user_id="9020", roles=("advisor",),
|
||||
customer_ids=("9001",), permissions=EMPLOYEE_PERMISSIONS,
|
||||
)
|
||||
|
||||
assert customer_memory_scope(only_capability) == ()
|
||||
assert customer_memory_scope(both) == (9001,)
|
||||
|
||||
|
||||
def test_employee_reads_assigned_customers_in_deterministic_order() -> None:
|
||||
context = _context(user_id="9020", roles=("advisor",), customer_ids=("9103", "9001", "9102"))
|
||||
context = _context(
|
||||
user_id="9020", roles=("advisor",), customer_ids=("9103", "9001", "9102"),
|
||||
permissions=EMPLOYEE_PERMISSIONS,
|
||||
)
|
||||
|
||||
# 升序:同一身份每次得到同一批次,不随数据库返回顺序漂移。
|
||||
assert customer_memory_scope(context) == (9001, 9102, 9103)
|
||||
@@ -55,7 +89,10 @@ def test_employee_reads_assigned_customers_in_deterministic_order() -> None:
|
||||
|
||||
def test_scope_is_truncated_by_ascending_customer_id() -> None:
|
||||
many = tuple(str(10000 + index) for index in range(MAX_RECALL_CUSTOMERS + 5))
|
||||
context = _context(user_id="9002", roles=("risk_operator",), customer_ids=many)
|
||||
context = _context(
|
||||
user_id="9002", roles=("risk_operator",), customer_ids=many,
|
||||
permissions=EMPLOYEE_PERMISSIONS,
|
||||
)
|
||||
|
||||
scope = customer_memory_scope(context)
|
||||
|
||||
@@ -69,6 +106,7 @@ def test_scope_dedupes_and_skips_dirty_values() -> None:
|
||||
user_id="9002",
|
||||
roles=("risk_operator",),
|
||||
customer_ids=("9102", "9102", "not-a-number", "", "0", "-5", "9103"),
|
||||
permissions=EMPLOYEE_PERMISSIONS,
|
||||
)
|
||||
|
||||
# 脏值跳过而不是让整次召回崩掉;0 与负数不是合法客户号。
|
||||
|
||||
Reference in New Issue
Block a user