fix: confine PDP checks to the admitted local transport
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a12d13-27f2-7163-b8c0-f682af02772c
This commit is contained in:
parent
0db6adef69
commit
d379a3dfba
6 changed files with 268 additions and 50 deletions
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
118
tests/test_decision_transport.py
Normal file
118
tests/test_decision_transport.py
Normal 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={})
|
||||
Loading…
Add table
Add a link
Reference in a new issue