fix: confine PDP checks to the admitted local transport
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a12d13-27f2-7163-b8c0-f682af02772c
This commit is contained in:
tegwick 2026-10-12 08:27:37 +02:00
parent 0db6adef69
commit d379a3dfba
6 changed files with 268 additions and 50 deletions

View file

@ -161,7 +161,6 @@ def test_unsupported_pdp_addresses_are_refused(url, fragment):
"url",
[
"http://127.0.0.1:18080",
"http://localhost:18080",
"http://[::1]:18080",
],
)

View file

@ -0,0 +1,118 @@
"""Exercise the real HTTP boundary with synthetic credentials and local servers."""
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
from types import SimpleNamespace
import pytest
from secrets_engine import approval_consume
from secrets_engine.decision_check import check_decision
from secrets_engine.errors import DecisionError
@contextmanager
def server(*, status=200, body=b'{}', location=None):
seen = []
class Handler(BaseHTTPRequestHandler):
def handle_request(self):
# Record only a boolean about our synthetic credential.
seen.append(self.headers.get('Authorization') == 'Bearer synthetic-pdp-credential')
self.rfile.read(int(self.headers.get('Content-Length', '0')))
self.send_response(status)
if location:
self.send_header('Location', location)
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
do_POST = do_GET = handle_request
def log_message(self, *_args):
pass
httpd = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = Thread(target=httpd.serve_forever, kwargs={'poll_interval': 0.01}, daemon=True)
thread.start()
try:
yield f'http://127.0.0.1:{httpd.server_port}', seen
finally:
httpd.shutdown()
httpd.server_close()
thread.join(timeout=2)
@pytest.fixture
def credential(tmp_path):
path = tmp_path / 'synthetic.token'
path.write_text('synthetic-pdp-credential')
path.chmod(0o600)
return path
@pytest.fixture(autouse=True)
def clear_proxies(monkeypatch):
# Tests must not contact proxies configured on the developer's machine.
import os
import urllib.request
monkeypatch.setattr(urllib.request, "_opener", None)
for name in os.environ:
if name.lower().endswith('_proxy'):
monkeypatch.delenv(name)
@pytest.mark.parametrize('status', [301, 302, 303, 307, 308])
def test_redirect_never_contacts_another_origin(credential, status):
with server() as (destination, received):
with server(status=status, location=destination + '/sink') as (source, sent):
with pytest.raises(DecisionError):
check_decision(base_url=source, token_file=credential, request={})
assert sent == [True]
assert received == []
def test_environment_proxy_never_receives_request(credential, monkeypatch):
with server() as (proxy, proxied):
with server() as (source, direct):
monkeypatch.setenv('http_proxy', proxy)
monkeypatch.setenv('no_proxy', '')
assert check_decision(base_url=source, token_file=credential, request={}) == {}
assert direct == [True]
assert proxied == []
def test_approval_flow_uses_the_same_safe_transport(credential, monkeypatch):
observation = SimpleNamespace(request={})
monkeypatch.setattr(approval_consume, 'resolve_approval_observation', lambda *a, **kw: observation)
with server() as (destination, received):
with server(status=302, location=destination + '/sink') as (source, sent):
cfg = SimpleNamespace(pdp_url=source, pdp_token_file=credential,
authorization_policy_package='test', authorization_policy_version='v1')
with pytest.raises(DecisionError):
approval_consume.authorize_action(cfg, object(), 'rotate', None)
assert sent == [True]
assert received == []
@pytest.mark.parametrize('url', [
'http://localhost:8080', 'http://localhost.:8080',
'http://user:synthetic-password@127.0.0.1:8080',
'http://127.0.0.1:8080?query=value', 'http://127.0.0.1:8080#fragment',
'http://127.0.0.1:8080/path', 'http://127.0.0.1:0',
'http://127.0.0.1:wrong', 'http://[::1', 'http://127.0.0.1:65536',
'http://127.0.0.1:\n8080', ' http://127.0.0.1:8080', 'file://127.0.0.1',
])
def test_invalid_address_refused_before_credential_read(url, tmp_path):
with pytest.raises(DecisionError, match='URL'):
check_decision(base_url=url, token_file=tmp_path / 'missing', request={})
@pytest.mark.parametrize('body,reason', [
(b'{}' + b' ' * (512 * 1024), 'too large'),
(b'\xff', 'non-JSON'), (b'[]', 'non-object'),
], ids=['oversized', 'invalid-utf8', 'array'])
def test_response_limits_and_encoding_fail_closed(credential, body, reason):
with server(body=body) as (source, _):
with pytest.raises(DecisionError, match=reason):
check_decision(base_url=source, token_file=credential, request={})