fix: refuse the sandbox rather than quietly running it as the superuser
Both reviews landed on the same line independently. _as_role swaps the credentials in the DSN with a regex, and when the pattern does not match it returned the string unchanged. Two shapes miss it: no inline credentials, and a unix socket URL. Either one is a legal DSN. What that costs is not a broken test. The sandbox builds, every assertion still passes, and bare_dsn is now the admin connection, so the worst-case case runs the real script with --apply as a superuser against the shared table. The verifier ran that command as a dry run to see what it would have done: target public.llm_calls, 11 rows to delete. The case would still have gone red on the exit code, after the rows were gone. It raises now. There is also a second check that connects and compares current_user, because a successful string substitution is not the same as connecting as that role -- PGUSER and friends still override. The whole design rests on that connection having no grant on the shared table; a string comparison is too thin a thing to rest it on. That check has to stay inside the try. Past it the cleanup statements have already been merged into the fixture-level stack, and unwinding again runs DROP OWNED BY twice, which has no IF EXISTS. The catalog probe took any SQL and ran it on the admin connection. The design claims withholding the DSN makes the boundary structural; that was only true of the connection string, not of the capability. It takes SELECT now. --table's schema half is restricted to plain identifiers. Not a security fix, since the name goes through a parameter and _quote: the help text says complex identifiers are unsupported and the code was accepting them anyway.
This commit is contained in:
@@ -10,6 +10,8 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.integration.conftest import _as_role
|
||||
|
||||
_DDL = "CREATE TABLE llm_calls (call_id TEXT PRIMARY KEY, created_at TIMESTAMPTZ DEFAULT now())"
|
||||
|
||||
|
||||
@@ -40,6 +42,42 @@ async def _oid_of_llm_calls(dsn: str) -> int | None:
|
||||
await conn.close()
|
||||
|
||||
|
||||
class TestRoleDsnConstruction:
|
||||
"""凭据替换失败必须**当场报错**,不许退回管理身份(合并前审查的 P1)。
|
||||
|
||||
这条防线的失效形态特别隐蔽: 替换不上时 `re.sub` 原样返回管理连接串,沙箱
|
||||
"看起来"建好了、用例照常绿,而 `bare_dsn` 其实是超级用户——最坏情况用例
|
||||
会拿它跑真实 `--apply`,把共享表删空之后才在 `assert returncode == 2` 上红。
|
||||
行已经没了。设计 §5.1 要的是"越界做不到",不是"越界会被发现"。
|
||||
"""
|
||||
|
||||
def test_inline_credentials_are_replaced(self):
|
||||
swapped = _as_role("postgresql://app:secret@h:5432/polygateway", "pgw_r_x")
|
||||
|
||||
assert swapped.startswith("postgresql://pgw_r_x:")
|
||||
assert "app:secret" not in swapped
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dsn",
|
||||
[
|
||||
"postgresql://h:5432/polygateway", # 口令走 PGPASSWORD / .pgpass / trust
|
||||
"postgresql:///polygateway?host=/var/run/postgresql", # unix socket
|
||||
],
|
||||
)
|
||||
def test_a_dsn_without_inline_credentials_is_refused(self, dsn):
|
||||
"""这两种都是合法 DSN,今天的 .env 恰好不是它们——恰好而已。"""
|
||||
with pytest.raises(RuntimeError, match="沙箱角色"):
|
||||
_as_role(dsn, "pgw_r_x")
|
||||
|
||||
|
||||
class TestCatalogProbeIsReadOnly:
|
||||
"""探针拿的是管理连接,故它只许查——否则"用例够不到管理能力"就是句空话。"""
|
||||
|
||||
async def test_non_select_statements_are_refused(self, pg_catalog_probe):
|
||||
with pytest.raises(RuntimeError, match="只接受 SELECT"):
|
||||
await pg_catalog_probe("DELETE FROM llm_calls WHERE call_id = 'nope'")
|
||||
|
||||
|
||||
class TestSchemaOnlySandbox:
|
||||
async def test_table_lands_in_the_sandbox_schema_and_bare_dsn_is_absent(self, pg_sandbox):
|
||||
"""`role="none"`: 表落在自建 schema 下;不发角色,故没有裸 DSN 可给。"""
|
||||
|
||||
Reference in New Issue
Block a user