Some checks are pending
CI / lint-and-test (push) Waiting to run
* Restrict leoclean runtime reads to queried columns * Repair leoclean runtime authority review findings * Harden leoclean query and CI permission contracts
822 lines
33 KiB
Python
822 lines
33 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from ops import verify_gcp_leoclean_runtime_permissions as verifier
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
ROLE_SQL = REPO_ROOT / "ops" / "gcp_leoclean_runtime_role.sql"
|
|
RUN_ENV = "TELEO_RUN_LEOCLEAN_RUNTIME_POSTGRES_CANARY"
|
|
POSTGRES_IMAGE = "postgres@sha256:eb4759788a2182f08257135e61a34f2cfc3c2914079f3465d64ee62350f4d081"
|
|
POSTGRES_VERSION_NUM = "160014"
|
|
DATABASE = "teleo_canonical"
|
|
RUNTIME_ROLE = "leoclean_kb_runtime"
|
|
LOCAL_RUNTIME_PASSWORD = "disposable-r2-runtime-password"
|
|
IDENTIFIER_RE = re.compile(r"[a-z_][a-z0-9_]*\Z")
|
|
|
|
|
|
def test_ci_runs_digest_pinned_permission_canary_and_always_cleans_up() -> None:
|
|
workflow_path = REPO_ROOT / ".github" / "workflows" / "ci.yml"
|
|
workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8"))
|
|
assert workflow["env"]["LEOCLEAN_RUNTIME_POSTGRES_IMAGE"] == POSTGRES_IMAGE
|
|
|
|
steps = workflow["jobs"]["test"]["steps"]
|
|
pull_step = next(
|
|
step for step in steps if step.get("name") == "Pull digest-pinned PostgreSQL permission canary image"
|
|
)
|
|
pytest_step = next(step for step in steps if step.get("name") == "Pytest")
|
|
cleanup_step = next(
|
|
step for step in steps if step.get("name") == "Remove interrupted PostgreSQL permission canaries"
|
|
)
|
|
|
|
assert pull_step["run"] == 'docker pull "${LEOCLEAN_RUNTIME_POSTGRES_IMAGE}"'
|
|
assert pytest_step["env"] == {RUN_ENV: "1"}
|
|
assert "python -m pytest" in pytest_step["run"]
|
|
assert cleanup_step["if"] == "always()"
|
|
assert "label=livingip.r2.permission-canary" in cleanup_step["run"]
|
|
assert "docker rm --force" in cleanup_step["run"]
|
|
|
|
|
|
def _quote_identifier(value: str) -> str:
|
|
assert IDENTIFIER_RE.fullmatch(value), value
|
|
return f'"{value}"'
|
|
|
|
|
|
def _run(
|
|
command: list[str],
|
|
*,
|
|
input_text: str | None = None,
|
|
timeout: int = 120,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
command,
|
|
input=input_text,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
timeout=timeout,
|
|
)
|
|
|
|
|
|
def _require_success(completed: subprocess.CompletedProcess[str], action: str) -> str:
|
|
if completed.returncode != 0:
|
|
raise AssertionError(
|
|
f"{action} failed with exit {completed.returncode}\n"
|
|
f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
|
|
)
|
|
return completed.stdout.strip()
|
|
|
|
|
|
def _psql(
|
|
container: str,
|
|
sql: str,
|
|
*,
|
|
role: str = "postgres",
|
|
database: str = DATABASE,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
return _run(
|
|
[
|
|
"docker",
|
|
"exec",
|
|
"-i",
|
|
container,
|
|
"psql",
|
|
"--no-psqlrc",
|
|
"--no-password",
|
|
"--set=ON_ERROR_STOP=1",
|
|
"--set=VERBOSITY=verbose",
|
|
"--tuples-only",
|
|
"--no-align",
|
|
"--username",
|
|
role,
|
|
"--dbname",
|
|
database,
|
|
],
|
|
input_text=sql,
|
|
)
|
|
|
|
|
|
def _assert_denied(container: str, sql: str, sqlstate: str = "42501") -> None:
|
|
completed = _psql(container, sql, role=RUNTIME_ROLE)
|
|
assert completed.returncode != 0, f"unexpected success for: {sql}"
|
|
assert re.search(rf"(?:ERROR|FATAL):\s+{re.escape(sqlstate)}:", completed.stderr), (
|
|
f"expected SQLSTATE {sqlstate} for {sql!r}\nstderr:\n{completed.stderr}"
|
|
)
|
|
|
|
|
|
def _generated_read_tables_sql() -> str:
|
|
statements: list[str] = []
|
|
for schema, relation, columns in verifier.READ_COLUMN_ALLOWLIST:
|
|
if (schema, relation) in {("public", "agents"), ("kb_stage", "kb_proposals")}:
|
|
continue
|
|
definitions = [f"{_quote_identifier(column)} text" for column in columns]
|
|
definitions.append('"runtime_hidden_marker" text')
|
|
statements.append(
|
|
f"create table {_quote_identifier(schema)}.{_quote_identifier(relation)} ({', '.join(definitions)});"
|
|
)
|
|
return "\n".join(statements)
|
|
|
|
|
|
def _bootstrap_sql() -> str:
|
|
# This fixture models the reviewed privilege topology, not production-schema parity.
|
|
return f"""
|
|
create role kb_review nologin noinherit;
|
|
create role kb_apply nologin noinherit;
|
|
create schema kb_stage;
|
|
|
|
create table public.agents (
|
|
id uuid primary key,
|
|
handle text not null unique,
|
|
runtime_hidden_marker text
|
|
);
|
|
|
|
{_generated_read_tables_sql()}
|
|
|
|
create table kb_stage.kb_proposals (
|
|
id uuid primary key default gen_random_uuid(),
|
|
proposal_type text not null,
|
|
status text not null default 'pending_review',
|
|
proposed_by_handle text,
|
|
proposed_by_agent_id uuid,
|
|
channel text not null default 'cli',
|
|
source_ref text,
|
|
rationale text not null,
|
|
payload jsonb not null,
|
|
reviewed_by_handle text,
|
|
reviewed_by_agent_id uuid,
|
|
reviewed_at timestamptz,
|
|
review_note text,
|
|
applied_by_handle text,
|
|
applied_by_agent_id uuid,
|
|
applied_at timestamptz,
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now(),
|
|
constraint kb_proposals_proposal_type_check check (
|
|
proposal_type in ('revise_claim', 'revise_strategy', 'add_edge')
|
|
),
|
|
constraint kb_proposals_status_check check (
|
|
status in ('pending_review', 'approved', 'rejected', 'applied', 'canceled')
|
|
),
|
|
constraint kb_proposals_proposed_by_agent_id_fkey
|
|
foreign key (proposed_by_agent_id) references public.agents(id),
|
|
constraint kb_proposals_reviewed_by_agent_id_fkey
|
|
foreign key (reviewed_by_agent_id) references public.agents(id),
|
|
constraint kb_proposals_applied_by_agent_id_fkey
|
|
foreign key (applied_by_agent_id) references public.agents(id)
|
|
);
|
|
|
|
create table public.private_notes (id uuid primary key, body text not null);
|
|
|
|
insert into public.agents (id, handle, runtime_hidden_marker)
|
|
values ('11111111-1111-4111-8111-111111111111', 'leo', 'not-runtime-readable');
|
|
|
|
create function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text)
|
|
returns jsonb language sql security definer set search_path = pg_catalog, pg_temp
|
|
as $function$ select '{{}}'::jsonb $function$;
|
|
|
|
create function kb_stage.assert_approved_proposal(
|
|
uuid, text, jsonb, text, uuid, timestamptz, text
|
|
) returns void language plpgsql security definer set search_path = pg_catalog, pg_temp
|
|
as $function$ begin return; end; $function$;
|
|
|
|
create function kb_stage.finish_approved_proposal(
|
|
uuid, text, jsonb, text, uuid, timestamptz, text, text
|
|
) returns jsonb language sql security definer set search_path = pg_catalog, pg_temp
|
|
as $function$ select '{{}}'::jsonb $function$;
|
|
"""
|
|
|
|
|
|
def _read_fingerprint(container: str) -> dict[str, int]:
|
|
pairs: list[str] = []
|
|
for schema, relation, columns in verifier.READ_COLUMN_ALLOWLIST:
|
|
key = f"{schema}.{relation}"
|
|
first_column = _quote_identifier(columns[0])
|
|
qualified = f"{_quote_identifier(schema)}.{_quote_identifier(relation)}"
|
|
pairs.extend((f"'{key}'", f"(select count({first_column}) from {qualified})"))
|
|
sql = f"select jsonb_build_object({', '.join(pairs)})::text;"
|
|
output = _require_success(_psql(container, sql, role=RUNTIME_ROLE), "read fingerprint")
|
|
return json.loads(output)
|
|
|
|
|
|
def _provision(container: str, remote_sql_path: str) -> subprocess.CompletedProcess[str]:
|
|
return _run(
|
|
[
|
|
"docker",
|
|
"exec",
|
|
"--interactive",
|
|
container,
|
|
"psql",
|
|
"--no-psqlrc",
|
|
"--no-password",
|
|
"--set=ON_ERROR_STOP=1",
|
|
"--set=VERBOSITY=verbose",
|
|
"--username",
|
|
"postgres",
|
|
"--dbname",
|
|
DATABASE,
|
|
f"--file={remote_sql_path}",
|
|
],
|
|
input_text=f"{LOCAL_RUNTIME_PASSWORD}\n{LOCAL_RUNTIME_PASSWORD}\n",
|
|
)
|
|
|
|
|
|
def _read_table_rows_state(container: str) -> dict[str, dict[str, object]]:
|
|
table_output = _require_success(
|
|
_psql(
|
|
container,
|
|
"""
|
|
select namespace.nspname || '|' || relation.relname
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind in ('r', 'p')
|
|
order by namespace.nspname, relation.relname;
|
|
""",
|
|
),
|
|
"provisioning table inventory",
|
|
)
|
|
state: dict[str, dict[str, object]] = {}
|
|
for line in table_output.splitlines():
|
|
schema, relation = line.strip().split("|", 1)
|
|
assert IDENTIFIER_RE.fullmatch(schema), schema
|
|
assert IDENTIFIER_RE.fullmatch(relation), relation
|
|
qualified = f"{_quote_identifier(schema)}.{_quote_identifier(relation)}"
|
|
output = _require_success(
|
|
_psql(
|
|
container,
|
|
f"""
|
|
select pg_catalog.jsonb_build_object(
|
|
'row_count', pg_catalog.count(*),
|
|
'rowset_md5', pg_catalog.md5(
|
|
coalesce(
|
|
pg_catalog.string_agg(
|
|
pg_catalog.to_jsonb(row_data)::text,
|
|
E'\\n'
|
|
order by pg_catalog.to_jsonb(row_data)::text
|
|
),
|
|
''
|
|
)
|
|
)
|
|
)::text
|
|
from {qualified} as row_data;
|
|
""",
|
|
),
|
|
f"row-state snapshot for {schema}.{relation}",
|
|
)
|
|
state[f"{schema}.{relation}"] = json.loads(output)
|
|
return state
|
|
|
|
|
|
def _read_catalog_state(container: str) -> dict[str, object]:
|
|
output = _require_success(
|
|
_psql(
|
|
container,
|
|
"""
|
|
with scoped_role_names(role_name) as (
|
|
values ('leoclean_kb_runtime'), ('leoclean_kb_stage_owner')
|
|
), acl_entries(kind, object_name, grantor_name, grantee_name, privilege_type, is_grantable) as (
|
|
select 'database',
|
|
database_row.datname,
|
|
grantor_role.rolname,
|
|
case when acl.grantee = 0 then 'PUBLIC' else grantee_role.rolname end,
|
|
acl.privilege_type,
|
|
acl.is_grantable
|
|
from pg_catalog.pg_database database_row
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(
|
|
database_row.datacl,
|
|
pg_catalog.acldefault('d', database_row.datdba)
|
|
)
|
|
) acl
|
|
left join pg_catalog.pg_roles grantor_role on grantor_role.oid = acl.grantor
|
|
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
|
union all
|
|
select 'schema',
|
|
namespace.nspname,
|
|
grantor_role.rolname,
|
|
case when acl.grantee = 0 then 'PUBLIC' else grantee_role.rolname end,
|
|
acl.privilege_type,
|
|
acl.is_grantable
|
|
from pg_catalog.pg_namespace namespace
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(namespace.nspacl, pg_catalog.acldefault('n', namespace.nspowner))
|
|
) acl
|
|
left join pg_catalog.pg_roles grantor_role on grantor_role.oid = acl.grantor
|
|
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
union all
|
|
select case when relation.relkind = 'S' then 'sequence' else 'relation' end,
|
|
pg_catalog.format('%I.%I', namespace.nspname, relation.relname),
|
|
grantor_role.rolname,
|
|
case when acl.grantee = 0 then 'PUBLIC' else grantee_role.rolname end,
|
|
acl.privilege_type,
|
|
acl.is_grantable
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(
|
|
relation.relacl,
|
|
pg_catalog.acldefault(case when relation.relkind = 'S' then 'S'::"char" else 'r'::"char" end, relation.relowner)
|
|
)
|
|
) acl
|
|
left join pg_catalog.pg_roles grantor_role on grantor_role.oid = acl.grantor
|
|
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f', 'S')
|
|
union all
|
|
select 'column',
|
|
pg_catalog.format('%I.%I.%I', namespace.nspname, relation.relname, attribute.attname),
|
|
grantor_role.rolname,
|
|
case when acl.grantee = 0 then 'PUBLIC' else grantee_role.rolname end,
|
|
acl.privilege_type,
|
|
acl.is_grantable
|
|
from pg_catalog.pg_attribute attribute
|
|
join pg_catalog.pg_class relation on relation.oid = attribute.attrelid
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
cross join lateral pg_catalog.aclexplode(attribute.attacl) acl
|
|
left join pg_catalog.pg_roles grantor_role on grantor_role.oid = acl.grantor
|
|
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
union all
|
|
select 'routine',
|
|
pg_catalog.format(
|
|
'%I.%I(%s)',
|
|
namespace.nspname,
|
|
function_row.proname,
|
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid)
|
|
),
|
|
grantor_role.rolname,
|
|
case when acl.grantee = 0 then 'PUBLIC' else grantee_role.rolname end,
|
|
acl.privilege_type,
|
|
acl.is_grantable
|
|
from pg_catalog.pg_proc function_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(function_row.proacl, pg_catalog.acldefault('f', function_row.proowner))
|
|
) acl
|
|
left join pg_catalog.pg_roles grantor_role on grantor_role.oid = acl.grantor
|
|
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
union all
|
|
select 'large_object',
|
|
metadata.oid::text,
|
|
grantor_role.rolname,
|
|
case when acl.grantee = 0 then 'PUBLIC' else grantee_role.rolname end,
|
|
acl.privilege_type,
|
|
acl.is_grantable
|
|
from pg_catalog.pg_largeobject_metadata metadata
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(metadata.lomacl, pg_catalog.acldefault('L', metadata.lomowner))
|
|
) acl
|
|
left join pg_catalog.pg_roles grantor_role on grantor_role.oid = acl.grantor
|
|
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
|
union all
|
|
select 'parameter',
|
|
parameter_acl.parname,
|
|
grantor_role.rolname,
|
|
case when acl.grantee = 0 then 'PUBLIC' else grantee_role.rolname end,
|
|
acl.privilege_type,
|
|
acl.is_grantable
|
|
from pg_catalog.pg_parameter_acl parameter_acl
|
|
cross join lateral pg_catalog.aclexplode(parameter_acl.paracl) acl
|
|
left join pg_catalog.pg_roles grantor_role on grantor_role.oid = acl.grantor
|
|
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
|
), role_state as (
|
|
select coalesce(
|
|
pg_catalog.jsonb_agg(
|
|
pg_catalog.jsonb_build_object(
|
|
'role', role_row.rolname,
|
|
'can_login', role_row.rolcanlogin,
|
|
'is_superuser', role_row.rolsuper,
|
|
'can_create_db', role_row.rolcreatedb,
|
|
'can_create_role', role_row.rolcreaterole,
|
|
'inherits_privileges', role_row.rolinherit,
|
|
'can_replicate', role_row.rolreplication,
|
|
'bypasses_rls', role_row.rolbypassrls,
|
|
'connection_limit', role_row.rolconnlimit,
|
|
'role_config', role_row.rolconfig,
|
|
'password_is_null', auth_row.rolpassword is null,
|
|
'password_is_scram', coalesce(auth_row.rolpassword like 'SCRAM-SHA-256$%', false)
|
|
)
|
|
order by role_row.rolname
|
|
),
|
|
'[]'::pg_catalog.jsonb
|
|
) as value
|
|
from pg_catalog.pg_roles role_row
|
|
join pg_catalog.pg_authid auth_row on auth_row.oid = role_row.oid
|
|
join scoped_role_names scoped on scoped.role_name = role_row.rolname
|
|
), database_role_settings as (
|
|
select coalesce(
|
|
pg_catalog.jsonb_agg(
|
|
pg_catalog.jsonb_build_object(
|
|
'role', role_row.rolname,
|
|
'database', database_row.datname,
|
|
'setting', setting_value.setting
|
|
)
|
|
order by role_row.rolname, database_row.datname, setting_value.setting
|
|
),
|
|
'[]'::pg_catalog.jsonb
|
|
) as value
|
|
from pg_catalog.pg_db_role_setting setting_row
|
|
join pg_catalog.pg_roles role_row on role_row.oid = setting_row.setrole
|
|
join scoped_role_names scoped on scoped.role_name = role_row.rolname
|
|
left join pg_catalog.pg_database database_row on database_row.oid = setting_row.setdatabase
|
|
cross join lateral pg_catalog.unnest(setting_row.setconfig) setting_value(setting)
|
|
), membership_state as (
|
|
select coalesce(
|
|
pg_catalog.jsonb_agg(
|
|
pg_catalog.jsonb_build_object(
|
|
'granted_role', granted_role.rolname,
|
|
'member_role', member_role.rolname,
|
|
'grantor_role', grantor_role.rolname,
|
|
'admin_option', membership.admin_option,
|
|
'inherit_option', membership.inherit_option,
|
|
'set_option', membership.set_option
|
|
)
|
|
order by granted_role.rolname, member_role.rolname, grantor_role.rolname
|
|
),
|
|
'[]'::pg_catalog.jsonb
|
|
) as value
|
|
from pg_catalog.pg_auth_members membership
|
|
join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
|
|
join pg_catalog.pg_roles member_role on member_role.oid = membership.member
|
|
join pg_catalog.pg_roles grantor_role on grantor_role.oid = membership.grantor
|
|
where granted_role.rolname in (select role_name from scoped_role_names)
|
|
or member_role.rolname in (select role_name from scoped_role_names)
|
|
), ownership_state as (
|
|
select coalesce(
|
|
pg_catalog.jsonb_agg(
|
|
pg_catalog.jsonb_build_object(
|
|
'kind', owned.kind,
|
|
'object', owned.object_name,
|
|
'owner', owned.owner_name
|
|
)
|
|
order by owned.kind, owned.object_name, owned.owner_name
|
|
),
|
|
'[]'::pg_catalog.jsonb
|
|
) as value
|
|
from (
|
|
select 'relation' as kind,
|
|
pg_catalog.format('%I.%I', namespace.nspname, relation.relname) as object_name,
|
|
owner_role.rolname as owner_name
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = relation.relowner
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
union all
|
|
select 'routine',
|
|
pg_catalog.format(
|
|
'%I.%I(%s)',
|
|
namespace.nspname,
|
|
function_row.proname,
|
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid)
|
|
),
|
|
owner_role.rolname
|
|
from pg_catalog.pg_proc function_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = function_row.proowner
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
) owned
|
|
), normalized_acls as (
|
|
select coalesce(
|
|
pg_catalog.jsonb_agg(
|
|
pg_catalog.jsonb_build_object(
|
|
'kind', entries.kind,
|
|
'object', entries.object_name,
|
|
'grantor', entries.grantor_name,
|
|
'grantee', entries.grantee_name,
|
|
'privilege', entries.privilege_type,
|
|
'grantable', entries.is_grantable
|
|
)
|
|
order by entries.kind,
|
|
entries.object_name,
|
|
entries.grantor_name,
|
|
entries.grantee_name,
|
|
entries.privilege_type,
|
|
entries.is_grantable
|
|
),
|
|
'[]'::pg_catalog.jsonb
|
|
) as value
|
|
from acl_entries entries
|
|
)
|
|
select pg_catalog.jsonb_build_object(
|
|
'roles', (select value from role_state),
|
|
'database_role_settings', (select value from database_role_settings),
|
|
'memberships', (select value from membership_state),
|
|
'ownership', (select value from ownership_state),
|
|
'acls', (select value from normalized_acls),
|
|
'stage_function', pg_catalog.jsonb_build_object(
|
|
'owner', (
|
|
select owner_role.rolname
|
|
from pg_catalog.pg_proc function_row
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = function_row.proowner
|
|
where function_row.oid = 'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)'::pg_catalog.regprocedure
|
|
),
|
|
'runtime_execute', pg_catalog.has_function_privilege(
|
|
'leoclean_kb_runtime',
|
|
'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)',
|
|
'EXECUTE'
|
|
),
|
|
'owner_execute', pg_catalog.has_function_privilege(
|
|
'leoclean_kb_stage_owner',
|
|
'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)',
|
|
'EXECUTE'
|
|
)
|
|
)
|
|
)::text;
|
|
""",
|
|
),
|
|
"provisioning catalog-state snapshot",
|
|
)
|
|
return json.loads(output)
|
|
|
|
|
|
def _read_provisioning_state(container: str) -> dict[str, object]:
|
|
return {
|
|
"rows": _read_table_rows_state(container),
|
|
"catalog": _read_catalog_state(container),
|
|
}
|
|
|
|
|
|
def _state_with_runtime_login(state: dict[str, object], *, can_login: bool) -> dict[str, object]:
|
|
adjusted = json.loads(json.dumps(state))
|
|
roles = adjusted["catalog"]["roles"]
|
|
runtime = next(role for role in roles if role["role"] == RUNTIME_ROLE)
|
|
runtime["can_login"] = can_login
|
|
return adjusted
|
|
|
|
|
|
def _assert_exact_read_contract(container: str) -> None:
|
|
for schema, relation, columns in verifier.READ_COLUMN_ALLOWLIST:
|
|
qualified = f"{_quote_identifier(schema)}.{_quote_identifier(relation)}"
|
|
projection = ", ".join(_quote_identifier(column) for column in columns)
|
|
_require_success(
|
|
_psql(container, f"select {projection} from {qualified} limit 0;", role=RUNTIME_ROLE),
|
|
f"allowed projection {schema}.{relation}",
|
|
)
|
|
|
|
_assert_denied(container, f"select * from {qualified} limit 0;")
|
|
denied_column = (
|
|
"reviewed_by_agent_id" if (schema, relation) == ("kb_stage", "kb_proposals") else "runtime_hidden_marker"
|
|
)
|
|
_assert_denied(container, f"select {_quote_identifier(denied_column)} from {qualified} limit 0;")
|
|
|
|
|
|
def _assert_proposal_function_and_rollback(container: str) -> None:
|
|
calls = []
|
|
for proposal_type in ("revise_claim", "revise_strategy", "add_edge"):
|
|
calls.append(
|
|
"select concat_ws('|', staged->>'proposal_type', staged->>'status', "
|
|
"staged->>'proposed_by_handle', staged->>'channel') "
|
|
"from (select kb_stage.stage_leoclean_proposal("
|
|
f"'{proposal_type}', '', 'r2:{proposal_type}', 'permission canary', "
|
|
"'{\"canary\": true}'::jsonb) as staged) probe;"
|
|
)
|
|
sql = "\n".join(
|
|
[
|
|
"begin;",
|
|
*calls,
|
|
"select 'inside=' || count(id) from kb_stage.kb_proposals;",
|
|
"rollback;",
|
|
"select 'after=' || count(id) from kb_stage.kb_proposals;",
|
|
]
|
|
)
|
|
output = _require_success(_psql(container, sql, role=RUNTIME_ROLE), "proposal function rollback")
|
|
lines = [line.strip() for line in output.splitlines() if line.strip()]
|
|
assert "revise_claim|pending_review|leo|telegram" in lines
|
|
assert "revise_strategy|pending_review|leo|telegram" in lines
|
|
assert "add_edge|pending_review|leo|telegram" in lines
|
|
assert "inside=3" in lines
|
|
assert "after=0" in lines
|
|
|
|
|
|
def _assert_write_and_escalation_denials(container: str) -> None:
|
|
direct_writes = (
|
|
"insert into public.claims (id) values ('direct-canonical-write');",
|
|
"update public.claims set runtime_hidden_marker = 'x' where false;",
|
|
"delete from public.claims where false;",
|
|
"truncate public.claims;",
|
|
"insert into kb_stage.kb_proposals (proposal_type, rationale, payload) values ('revise_claim', 'direct-stage-write', '{}'::jsonb);",
|
|
"update kb_stage.kb_proposals set rationale = 'x' where false;",
|
|
"delete from kb_stage.kb_proposals where false;",
|
|
"truncate kb_stage.kb_proposals;",
|
|
)
|
|
for sql in direct_writes:
|
|
_assert_denied(container, sql)
|
|
|
|
escalation_attempts = (
|
|
"set role leoclean_kb_stage_owner;",
|
|
"set role kb_review;",
|
|
"set role kb_apply;",
|
|
"set role postgres;",
|
|
"create role r2_escape;",
|
|
"set session authorization postgres;",
|
|
"select kb_stage.approve_strict_proposal('00000000-0000-0000-0000-000000000000', 'leo', '{}'::jsonb, 'approve', 'note');",
|
|
"select kb_stage.assert_approved_proposal('00000000-0000-0000-0000-000000000000', 'revise_claim', '{}'::jsonb, 'leo', '11111111-1111-4111-8111-111111111111', now(), 'note');",
|
|
"select kb_stage.finish_approved_proposal('00000000-0000-0000-0000-000000000000', 'revise_claim', '{}'::jsonb, 'leo', '11111111-1111-4111-8111-111111111111', now(), 'note', 'applied');",
|
|
)
|
|
for sql in escalation_attempts:
|
|
_assert_denied(container, sql)
|
|
|
|
_assert_denied(
|
|
container,
|
|
"select kb_stage.stage_leoclean_proposal('leo', 'revise_claim', 'telegram', 'r2', 'forged', '{}'::jsonb);",
|
|
"42883",
|
|
)
|
|
_assert_denied(container, "select body from public.private_notes;", "42501")
|
|
|
|
|
|
@pytest.mark.skipif(os.environ.get(RUN_ENV) != "1", reason=f"set {RUN_ENV}=1 for the local PG16 canary")
|
|
def test_digest_pinned_network_disabled_postgres_16_14_permission_contract(tmp_path: Path) -> None:
|
|
assert shutil.which("docker") is not None, "Docker is required for the disposable permission canary"
|
|
|
|
image_output = _require_success(
|
|
_run(["docker", "image", "inspect", "--format", "{{.Id}}|{{json .RepoDigests}}", POSTGRES_IMAGE]),
|
|
"pinned image inspection",
|
|
)
|
|
image_id, repo_digests_json = image_output.split("|", 1)
|
|
assert re.fullmatch(r"sha256:[0-9a-f]{64}", image_id)
|
|
assert POSTGRES_IMAGE in json.loads(repo_digests_json)
|
|
|
|
run_token = uuid.uuid4().hex[:12]
|
|
container = f"leoclean-r2-{run_token}"
|
|
label = f"livingip.r2.permission-canary={run_token}"
|
|
cleanup_error: str | None = None
|
|
try:
|
|
started = _run(
|
|
[
|
|
"docker",
|
|
"run",
|
|
"--detach",
|
|
"--name",
|
|
container,
|
|
"--label",
|
|
label,
|
|
"--pull=never",
|
|
"--network=none",
|
|
"--tmpfs",
|
|
"/var/lib/postgresql/data:rw,noexec,nosuid,nodev,size=256m",
|
|
"--env",
|
|
"POSTGRES_DB=teleo_canonical",
|
|
"--env",
|
|
"POSTGRES_HOST_AUTH_METHOD=trust",
|
|
POSTGRES_IMAGE,
|
|
]
|
|
)
|
|
_require_success(started, "disposable PostgreSQL start")
|
|
|
|
inspect = json.loads(_require_success(_run(["docker", "inspect", container]), "container inspection"))[0]
|
|
assert inspect["Image"] == image_id
|
|
assert inspect["HostConfig"]["NetworkMode"] == "none"
|
|
assert not inspect["HostConfig"].get("PortBindings")
|
|
assert not inspect["HostConfig"].get("Binds")
|
|
tmpfs_spec = inspect["HostConfig"]["Tmpfs"]["/var/lib/postgresql/data"]
|
|
assert {"rw", "noexec", "nosuid", "nodev"}.issubset(tmpfs_spec.split(","))
|
|
assert any(size in tmpfs_spec.split(",") for size in ("size=256m", "size=268435456"))
|
|
assert all(mount["Type"] not in {"bind", "volume"} for mount in inspect["Mounts"])
|
|
|
|
last_startup_probe: subprocess.CompletedProcess[str] | None = None
|
|
for _ in range(120):
|
|
final_server = _run(
|
|
[
|
|
"docker",
|
|
"exec",
|
|
container,
|
|
"sh",
|
|
"-c",
|
|
'test "$(cat /proc/1/comm)" = postgres',
|
|
],
|
|
timeout=5,
|
|
)
|
|
if final_server.returncode == 0:
|
|
version = _psql(container, "show server_version_num;")
|
|
last_startup_probe = version
|
|
if version.returncode == 0 and version.stdout.strip() == POSTGRES_VERSION_NUM:
|
|
break
|
|
else:
|
|
last_startup_probe = final_server
|
|
time.sleep(0.25)
|
|
else:
|
|
logs = _run(["docker", "logs", container], timeout=10)
|
|
probe_output = ""
|
|
if last_startup_probe is not None:
|
|
probe_output = f"\nlast probe stdout:\n{last_startup_probe.stdout}\nlast probe stderr:\n{last_startup_probe.stderr}"
|
|
raise AssertionError(
|
|
f"final PostgreSQL server did not report version {POSTGRES_VERSION_NUM}"
|
|
f"{probe_output}\ncontainer logs:\n{logs.stdout}\n{logs.stderr}"
|
|
)
|
|
|
|
_require_success(_psql(container, "create database teleo_kb;"), "legacy database fixture")
|
|
_require_success(_psql(container, _bootstrap_sql()), "permission fixture bootstrap")
|
|
_require_success(
|
|
_run(["docker", "cp", str(ROLE_SQL), f"{container}:/tmp/gcp_leoclean_runtime_role.sql"]),
|
|
"role SQL copy",
|
|
)
|
|
|
|
failure_anchor = "\n-- LOGIN is the last state change"
|
|
role_sql = ROLE_SQL.read_text(encoding="utf-8")
|
|
assert role_sql.count(failure_anchor) == 1
|
|
failure_sql = role_sql.replace(
|
|
failure_anchor,
|
|
"\ngrant update on public.private_notes to leoclean_kb_runtime;"
|
|
"\nselect 1 / 0 as injected_mid_provision_failure;"
|
|
f"{failure_anchor}",
|
|
)
|
|
failure_path = tmp_path / "gcp_leoclean_runtime_role_injected_failure.sql"
|
|
failure_path.write_text(failure_sql, encoding="utf-8")
|
|
_require_success(
|
|
_run(["docker", "cp", str(failure_path), f"{container}:/tmp/gcp_leoclean_runtime_role_failure.sql"]),
|
|
"failure-injected role SQL copy",
|
|
)
|
|
|
|
successful_states: list[dict[str, object]] = []
|
|
for provision_number in range(1, 4):
|
|
_require_success(
|
|
_provision(container, "/tmp/gcp_leoclean_runtime_role.sql"),
|
|
f"runtime role provisioning run {provision_number}",
|
|
)
|
|
state = _read_provisioning_state(container)
|
|
assert state["catalog"]["stage_function"] == {
|
|
"owner": "leoclean_kb_stage_owner",
|
|
"owner_execute": False,
|
|
"runtime_execute": True,
|
|
}
|
|
assert state["catalog"]["memberships"] == []
|
|
role_state = {role["role"]: role for role in state["catalog"]["roles"]}
|
|
assert role_state[RUNTIME_ROLE]["password_is_scram"] is True
|
|
assert role_state[RUNTIME_ROLE]["password_is_null"] is False
|
|
assert role_state["leoclean_kb_stage_owner"]["password_is_scram"] is False
|
|
assert role_state["leoclean_kb_stage_owner"]["password_is_null"] is True
|
|
successful_states.append(state)
|
|
|
|
assert successful_states[1:] == successful_states[:1] * 2
|
|
stable_state = successful_states[0]
|
|
|
|
failed = _provision(container, "/tmp/gcp_leoclean_runtime_role_failure.sql")
|
|
assert failed.returncode != 0
|
|
assert re.search(r"ERROR:\s+22012:", failed.stderr)
|
|
assert _read_provisioning_state(container) == _state_with_runtime_login(stable_state, can_login=False)
|
|
disabled_login = _psql(container, "select 1;", role=RUNTIME_ROLE)
|
|
assert disabled_login.returncode != 0
|
|
assert f'role "{RUNTIME_ROLE}" is not permitted to log in' in disabled_login.stderr
|
|
|
|
_require_success(
|
|
_provision(container, "/tmp/gcp_leoclean_runtime_role.sql"),
|
|
"runtime role recovery provisioning",
|
|
)
|
|
assert _read_provisioning_state(container) == stable_state
|
|
|
|
identity = _require_success(
|
|
_psql(container, "select current_user || '|' || current_database();", role=RUNTIME_ROLE),
|
|
"runtime identity",
|
|
)
|
|
assert identity == f"{RUNTIME_ROLE}|{DATABASE}"
|
|
|
|
stage_definition = json.loads(
|
|
_require_success(
|
|
_psql(container, verifier._stage_function_definition_sql(), role=RUNTIME_ROLE),
|
|
"external verifier stage-function query",
|
|
)
|
|
)
|
|
assert verifier._assert_stage_function_definition(stage_definition)["owner_execute"] is False
|
|
|
|
before = _read_fingerprint(container)
|
|
assert before == {
|
|
f"{schema}.{relation}": (1 if (schema, relation) == ("public", "agents") else 0)
|
|
for schema, relation, _columns in verifier.READ_COLUMN_ALLOWLIST
|
|
}
|
|
|
|
_assert_exact_read_contract(container)
|
|
_assert_proposal_function_and_rollback(container)
|
|
_assert_write_and_escalation_denials(container)
|
|
|
|
_require_success(
|
|
_psql(container, "alter table public.claims add column future_private_text text;"),
|
|
"future-column sentinel",
|
|
)
|
|
_assert_denied(container, "select future_private_text from public.claims limit 0;")
|
|
assert _read_fingerprint(container) == before
|
|
finally:
|
|
removed = _run(["docker", "rm", "--force", container], timeout=30)
|
|
if removed.returncode != 0 and "No such container" not in f"{removed.stdout}\n{removed.stderr}":
|
|
cleanup_error = removed.stderr or removed.stdout
|
|
leftovers = _run(
|
|
["docker", "ps", "--all", "--filter", f"label={label}", "--format", "{{.Names}}"],
|
|
timeout=10,
|
|
)
|
|
if leftovers.returncode != 0 or leftovers.stdout.strip():
|
|
cleanup_error = cleanup_error or leftovers.stderr or leftovers.stdout
|
|
if cleanup_error is not None:
|
|
raise AssertionError(f"disposable PostgreSQL cleanup failed: {cleanup_error}")
|