Files
hunjing/backend/tests/integrations/sandbox/test_auth.py
T
2026-08-21 16:00:13 +08:00

207 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""授权凭证签发 / 校验单元测试。"""
from __future__ import annotations
import pytest
from app.integrations.sandbox.auth import (
AuthorizationContext,
compute_body_hash,
compute_file_op_hash,
sign_authorization,
verify_authorization,
)
SECRET = "python-rust-shared-secret"
# 固定向量,与 Rust sandbox/src/auth.rs 的 body_hash_known_vector_matches_python 共享
ARGV = ["sh", "-c", "echo hi"]
STDIN: str | None = None
def _ctx() -> AuthorizationContext:
return AuthorizationContext(
trace_id="trace_1",
risk_level="L1",
tool="shell",
character_id="character_01H",
run_id="run_01H",
)
def test_sign_then_verify_roundtrip() -> None:
token = sign_authorization(SECRET, _ctx(), ARGV, STDIN, ttl_seconds=60)
payload = verify_authorization(SECRET, token)
assert payload["trace_id"] == "trace_1"
assert payload["risk_level"] == "L1"
assert payload["tool"] == "shell"
def test_verify_rejects_wrong_secret() -> None:
token = sign_authorization(SECRET, _ctx(), ARGV, STDIN)
with pytest.raises(ValueError):
verify_authorization("other-secret", token)
def test_verify_rejects_tampered_signature() -> None:
token = sign_authorization(SECRET, _ctx(), ARGV, STDIN)
tampered = token[:-2] + ("AA" if token[-2:] != "AA" else "BB")
with pytest.raises(ValueError):
verify_authorization(SECRET, tampered)
def test_verify_rejects_expired() -> None:
token = sign_authorization(SECRET, _ctx(), ARGV, STDIN, ttl_seconds=-10)
with pytest.raises(ValueError):
verify_authorization(SECRET, token)
def test_verify_rejects_malformed() -> None:
with pytest.raises(ValueError):
verify_authorization(SECRET, "not-a-valid-token")
def test_sign_requires_secret() -> None:
with pytest.raises(ValueError):
sign_authorization("", _ctx(), ARGV, STDIN)
def test_payload_carries_body_hash() -> None:
token = sign_authorization(SECRET, _ctx(), ARGV, STDIN)
payload = verify_authorization(SECRET, token)
assert payload["body_hash"] == compute_body_hash(ARGV, STDIN)
def test_body_hash_differs_per_argv() -> None:
"""凭证绑死具体执行内容:不同 argv 产生不同 body_hash,杜绝重放任意命令。"""
t1 = sign_authorization(SECRET, _ctx(), ["sh", "-c", "echo a"], None)
t2 = sign_authorization(SECRET, _ctx(), ["sh", "-c", "echo b"], None)
p1 = verify_authorization(SECRET, t1)
p2 = verify_authorization(SECRET, t2)
assert p1["body_hash"] != p2["body_hash"]
def test_compute_body_hash_known_vector() -> None:
"""与 Rust compute_body_hash 共享固定向量,保证跨端字节级一致。"""
assert (
compute_body_hash(ARGV, STDIN)
== "f5fa97ea2ee92c505174672da52c59c952e49c32fa56a1db5bd983c10a829540"
)
def test_compute_body_hash_none_vs_empty_stdin() -> None:
assert compute_body_hash(["sh"], None) != compute_body_hash(["sh"], "")
def test_compute_file_op_hash_known_vector() -> None:
"""与 Rust compute_file_op_hash 共享固定向量,保证跨端字节级一致。"""
assert (
compute_file_op_hash("read", "data/note.txt")
== "717f67252c61cf1681cb3c6018d3801b79a511564664567b57d12d775bb8fa53"
)
def test_compute_file_op_hash_differs_per_op_and_path() -> None:
"""凭证绑死具体文件操作:op 或 path 不同则摘要不同。"""
assert compute_file_op_hash("read", "a.txt") != compute_file_op_hash("write", "a.txt")
assert compute_file_op_hash("read", "a.txt") != compute_file_op_hash("read", "b.txt")
def test_sign_carries_file_op_hash_only_for_file_ops() -> None:
"""exec 凭证 file_op_hash 为 None;文件凭证携带 {op, path} 摘要。"""
# exec 路径:argv 非 None → body_hash 绑定,file_op_hash 为 None
exec_token = sign_authorization(SECRET, _ctx(), ARGV, STDIN)
exec_payload = verify_authorization(SECRET, exec_token)
assert exec_payload["file_op_hash"] is None
assert exec_payload["body_hash"] == compute_body_hash(ARGV, STDIN)
# 文件路径:argv=None → body_hash 为 None,file_op_hash 绑定 {op, path}
file_token = sign_authorization(SECRET, _ctx(), None, None, file_op=("read", "data/note.txt"))
file_payload = verify_authorization(SECRET, file_token)
assert file_payload["body_hash"] is None
assert file_payload["file_op_hash"] == compute_file_op_hash("read", "data/note.txt")
def test_sign_carries_sorted_rlimit_overrides() -> None:
"""rlimit_overrides 随凭证下发,且按 key 排序(对齐 Rust serde BTreeMap 字母序)。"""
ctx = AuthorizationContext(
trace_id="trace_1",
risk_level="L1",
tool="python_runner",
# 反序插入,验证 sign 时被排序
rlimit_overrides={"fsize": 67_108_864, "as": 536_870_912},
)
token = sign_authorization(SECRET, ctx, ARGV, STDIN)
payload = verify_authorization(SECRET, token)
# 排序后 as 在 fsize 之前(字母序),与 Rust BTreeMap 序列化一致
assert payload["rlimit_overrides"] == {"as": 536_870_912, "fsize": 67_108_864}
# rlimit_overrides 属授权属性,不进 body_hash
assert payload["body_hash"] == compute_body_hash(ARGV, STDIN)
def test_sign_without_rlimit_overrides_is_null() -> None:
"""无 rlimit_overrides 时 payload 字段为 null(对齐 Rust Option None → serde null)。"""
token = sign_authorization(SECRET, _ctx(), ARGV, STDIN)
payload = verify_authorization(SECRET, token)
assert payload["rlimit_overrides"] is None
def test_compute_body_hash_code_matches_known_vectors() -> None:
"""与 Rust compute_body_hash_code 共享固定向量,保证跨端字节级一致。"""
from app.integrations.sandbox.auth import compute_body_hash_code
assert (
compute_body_hash_code("python", "print(1+1)", None)
== "e7d633605676c3b19c642ee2437d91f9fb22562aaffad40f217d84c587322b69"
)
assert (
compute_body_hash_code("python", "print(1+1)", "42")
== "f933764421a59d1578697189fb76374511c5d7e4f1ad03ad37da3241568c35d5"
)
assert (
compute_body_hash_code("node", "print(1+1)", None)
== "a0440aef4bbcf83d89127ddf89949019b6be939a1c02702d22b35b40b21862aa"
)
def test_sign_authorization_code_mode_carries_isolation_mode() -> None:
"""code 模式:body_hash 按 {language,code,stdin} 算;payload 末尾含 isolation_mode。"""
from app.integrations.sandbox.auth import (
AuthorizationContext,
compute_body_hash_code,
sign_authorization,
verify_authorization,
)
ctx = AuthorizationContext(
trace_id="trace_1",
risk_level="L3",
tool="shell",
isolation_mode="container",
)
token = sign_authorization(
"shared-secret",
ctx,
code="print(1+1)",
language="python",
stdin=None,
)
payload = verify_authorization("shared-secret", token)
assert list(payload)[-1] == "isolation_mode"
assert payload["isolation_mode"] == "container"
assert payload["body_hash"] == compute_body_hash_code("python", "print(1+1)", None)
def test_sign_authorization_argv_mode_backward_compatible_positional() -> None:
"""既有位置参数调用不受影响:argv/stdin 位置传、ttl_seconds 关键字传,仍可验签。"""
import base64
from app.integrations.sandbox.auth import AuthorizationContext, sign_authorization
ctx = AuthorizationContext(trace_id="t", risk_level="L1", tool="shell")
# 既有调用形式:argv/stdin 位置参数(与仓库现有 test_auth.py 一致)
token = sign_authorization("s", ctx, ["python3", "-c", "print(1)"], None)
payload_b64 = token.split(".", 1)[0]
pad = "=" * (-len(payload_b64) % 4)
payload_json = base64.urlsafe_b64decode(payload_b64 + pad).decode("utf-8")
# 末尾追加 isolation_mode(旧字段顺序不变),None 时为 null
assert payload_json.endswith('"isolation_mode":null}')