Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
605 lines
30 KiB
Python
605 lines
30 KiB
Python
import base64
|
|
import copy
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import time
|
|
import io
|
|
import os
|
|
import urllib.error
|
|
import signal
|
|
import threading
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
spec = importlib.util.spec_from_file_location('session', Path(__file__).with_name('openbao-negative-session.py'))
|
|
s = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(s)
|
|
|
|
|
|
def baseline():
|
|
reference = dict(s.registrations()[-1], clientId='secrets-engine-openbao',
|
|
secretRef='env:PRODUCTION', tokenLifetime='15m')
|
|
return '# preserve this comment\nclients:\n' + s.yaml.safe_dump([reference], sort_keys=False) + 'other: unchanged\n'
|
|
|
|
|
|
def envelope():
|
|
return {'proposal_id': s.PROPOSAL, 'target': s.TARGET, 'disposition': 'approved',
|
|
'founder_decision_id': 'fixture-decision',
|
|
'source_hashes': {str(Path(s.__file__).resolve()): s.digest(Path(s.__file__).read_bytes())},
|
|
'owner_acknowledgments': {o: 'fixture-owner-review' for o in
|
|
['net-kingdom', 'key-cape', 'railiance-platform']},
|
|
'valid_until_epoch': time.time() + 900, 'maximum_session_seconds': 900,
|
|
'bao_addr': 'http://openbao.openbao.svc:8200',
|
|
'bao_health_identity_sha256': s.digest(b'fixture-health-cluster'),
|
|
'role_observation': {'mount': 'keycape-services', 'role': 'secrets-engine-openbao',
|
|
'issuer': 'https://kc.coulomb.social', 'algorithm': 'RS256',
|
|
'bindings_exact': True, 'policy_exact': True, 'raw_expiration_leeway': 0,
|
|
'effective_expiration_leeway': 150, 'observed_at_epoch': time.time()}}
|
|
|
|
|
|
class Tests(unittest.TestCase):
|
|
def setUp(self):
|
|
s.REQUEST_DEADLINE = None
|
|
s.BUSINESS_MONOTONIC_DEADLINE = None
|
|
s.CLEANUP_MONOTONIC_DEADLINE = None
|
|
s.CLEANING = False
|
|
def test_preserve_original_bytes(self):
|
|
raw = baseline()
|
|
result = s.candidate_config(raw)
|
|
start = result.index('clientId: openbao-probe-aud')
|
|
self.assertIn('# preserve this comment', result[:start])
|
|
parsed = s.yaml.safe_load(result)
|
|
self.assertEqual(parsed['clients'][:-6], s.yaml.safe_load(raw)['clients'])
|
|
self.assertEqual(parsed['other'], 'unchanged')
|
|
|
|
def test_isolation_and_sixth_matching_expiry(self):
|
|
clients = s.registrations()
|
|
self.assertNotIn('audience', clients[0])
|
|
for c in clients[1:]:
|
|
self.assertEqual(c['audience'], 'secrets-engine-openbao')
|
|
for key in ['roles', 'tenant', 'serviceSubject', 'allowedScopes']:
|
|
self.assertEqual(clients[-1][key], s.yaml.safe_load(baseline())['clients'][0][key])
|
|
|
|
def test_preexisting_probe_refused(self):
|
|
raw = baseline().replace('other: unchanged', '- clientId: openbao-probe-aud\nother: unchanged')
|
|
with self.assertRaises(s.Refused):
|
|
s.candidate_config(raw)
|
|
|
|
def test_duplicate_yaml_refused(self):
|
|
with self.assertRaises(s.Refused):
|
|
s.candidate_config(baseline() + 'other: shadow\n')
|
|
|
|
def test_production_drift_refused(self):
|
|
with self.assertRaises(s.Refused):
|
|
s.candidate_config(baseline().replace('tenant:platform', 'tenant:other'))
|
|
|
|
def test_env_collision_refused(self):
|
|
spec = {'replicas': 1, 'strategy': {'type': 'Recreate'}, 'template': {'spec': {'containers': [
|
|
{'name': 'keycape', 'env': [{'name': s.ENVS[0], 'value': 'fixture'}]}]}}}
|
|
with self.assertRaises(s.Refused):
|
|
s.candidate_spec(spec)
|
|
|
|
def test_recovery_roundtrip_preserves_every_baseline_byte(self):
|
|
raw = baseline()
|
|
self.assertEqual(s.recovered_config(s.candidate_config(raw)), raw)
|
|
self.assertEqual(s.recovered_config(raw), raw)
|
|
|
|
def test_recovery_claim_drift_and_partial_registration_refused(self):
|
|
raw = s.candidate_config(baseline())
|
|
with self.assertRaises(s.Refused):
|
|
s.recovered_config(raw.replace('service:openbao-probe', 'service:changed'))
|
|
with self.assertRaises(s.Refused):
|
|
s.recovered_config(raw.replace('clientId: openbao-probe-aud', 'clientId: different-probe'))
|
|
|
|
def test_recovery_env_roundtrip(self):
|
|
spec = {'replicas': 1, 'strategy': {'type': 'Recreate'}, 'template': {'spec': {'containers': [
|
|
{'name': 'keycape', 'env': [{'name': 'ORIGINAL', 'value': 'fixture'}]}]}}}
|
|
self.assertEqual(s.recovered_spec(s.candidate_spec(spec)), spec)
|
|
|
|
def test_approval_is_not_optional(self):
|
|
e = envelope()
|
|
del e['founder_decision_id']
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e)
|
|
|
|
def test_source_change_refused(self):
|
|
e = envelope()
|
|
e['source_hashes'][str(Path(s.__file__).resolve())] = '0' * 64
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e)
|
|
|
|
def test_owner_missing_refused(self):
|
|
e = envelope()
|
|
del e['owner_acknowledgments']['net-kingdom']
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e)
|
|
|
|
def test_leeway_default_and_disabled(self):
|
|
e = envelope()
|
|
self.assertEqual(s.verify_sources(e), 150)
|
|
e['role_observation'].update(raw_expiration_leeway=-1, effective_expiration_leeway=0)
|
|
self.assertEqual(s.verify_sources(e), 0)
|
|
|
|
def test_stale_role_refused(self):
|
|
e = envelope()
|
|
e['role_observation']['observed_at_epoch'] -= 301
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e)
|
|
|
|
def test_wrong_effective_leeway_refused(self):
|
|
e = envelope()
|
|
e['role_observation']['effective_expiration_leeway'] = 0
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e)
|
|
|
|
def test_bao_credentials_in_origin_refused(self):
|
|
e = envelope()
|
|
e['bao_addr'] = 'https://user:password@example.invalid'
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e)
|
|
|
|
def test_future_role_observation_refused(self):
|
|
e = envelope()
|
|
e['role_observation']['observed_at_epoch'] = time.time() + 60
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e)
|
|
|
|
def test_arbitrary_https_origin_refused(self):
|
|
e = envelope()
|
|
e['bao_addr'] = 'https://attacker.invalid'
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e)
|
|
|
|
def test_hostile_proxy_is_disabled(self):
|
|
build = s.urllib.request.build_opener
|
|
with patch.dict(os.environ, {'HTTPS_PROXY':'http://hostile-proxy.invalid:8000',
|
|
'HTTP_PROXY':'http://hostile-proxy.invalid:8000'}), \
|
|
patch.object(s.urllib.request, 'build_opener', wraps=build) as observed:
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200')
|
|
handlers = observed.call_args.args
|
|
self.assertIsInstance(handlers[0], s.urllib.request.ProxyHandler)
|
|
self.assertEqual(handlers[0].proxies, {})
|
|
self.assertIsInstance(handlers[1], s.NoRedirect)
|
|
self.assertIsNone(handlers[1].redirect_request(None, None, 302, '', {}, 'https://hostile.invalid'))
|
|
|
|
def test_redirect_never_follows_or_counts_as_refusal(self):
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200')
|
|
response = urllib.error.HTTPError('https://kc.coulomb.social/token', 302,
|
|
'redirect', {}, io.BytesIO(b'{}'))
|
|
with patch.object(transport.opener, 'open', side_effect=response) as observed:
|
|
status, raw = transport.post('https://kc.coulomb.social/token', form={})
|
|
self.assertEqual(status, 302)
|
|
self.assertEqual(observed.call_count, 1)
|
|
|
|
def test_oversized_http_body_is_closed_and_refused(self):
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200')
|
|
response = type('Response', (), {'getcode':lambda self: 200,
|
|
'read':lambda self, size: b'x' * size, 'close':lambda self: setattr(self, 'closed', True)})()
|
|
with patch.object(transport.opener, 'open', return_value=response):
|
|
with self.assertRaisesRegex(s.Refused, 'http_response_oversized'):
|
|
transport.post('https://kc.coulomb.social/token', form={})
|
|
self.assertTrue(response.closed)
|
|
|
|
def test_expired_window_prevents_protected_kubernetes_and_http(self):
|
|
s.REQUEST_DEADLINE = time.time() - 1
|
|
with patch.object(s.subprocess, 'run') as backend:
|
|
with self.assertRaisesRegex(s.Refused, 'approved_window_expired'):
|
|
s.k(['create', '-f', '/dev/stdin'], {})
|
|
backend.assert_not_called()
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200')
|
|
with patch.object(transport.opener, 'open') as backend:
|
|
with self.assertRaisesRegex(s.Refused, 'approved_window_expired'):
|
|
transport.post('https://kc.coulomb.social/token', form={})
|
|
backend.assert_not_called()
|
|
|
|
def test_cleanup_revoke_allowed_after_window_expiry(self):
|
|
s.REQUEST_DEADLINE = time.time() - 1
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200')
|
|
response = type('Response', (), {'getcode':lambda self: 204,
|
|
'read':lambda self, size: b'', 'close':lambda self: None})()
|
|
with patch.object(transport.opener, 'open', return_value=response):
|
|
self.assertEqual(transport.post('http://openbao.openbao.svc:8200/v1/auth/token/revoke-self'), (204, b''))
|
|
|
|
def test_http_read_has_finite_size(self):
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200')
|
|
sizes = []
|
|
response = type('Response', (), {'getcode':lambda self: 200,
|
|
'read':lambda self, size: sizes.append(size) or b'{}', 'close':lambda self: None})()
|
|
with patch.object(transport.opener, 'open', return_value=response):
|
|
transport.post('https://kc.coulomb.social/token')
|
|
self.assertEqual(sizes, [s.MAX_RESPONSE + 1])
|
|
|
|
def test_nonfinite_and_boolean_timestamps_refused(self):
|
|
for value in [float('inf'), float('-inf'), float('nan'), True, False, 'Infinity', 10 ** 1000]:
|
|
for field in ['valid_until_epoch', 'observed_at_epoch']:
|
|
with self.subTest(field=field, value_type=type(value).__name__):
|
|
e = envelope()
|
|
if field == 'valid_until_epoch':
|
|
e[field] = value
|
|
else:
|
|
e['role_observation'][field] = value
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e)
|
|
|
|
def test_finite_far_future_and_noninteger_window_refused(self):
|
|
e = envelope()
|
|
e['valid_until_epoch'] = time.time() + 86400
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e)
|
|
for value in [True, 900.0, float('inf'), float('nan'), '900']:
|
|
e = envelope()
|
|
e['maximum_session_seconds'] = value
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e)
|
|
|
|
def test_recovery_also_requires_finite_typed_window(self):
|
|
e = envelope()
|
|
e['disposition'] = 'recovery-approved'
|
|
e['valid_until_epoch'] = float('inf')
|
|
with self.assertRaises(s.Refused):
|
|
s.verify_sources(e, recovery=True)
|
|
|
|
def test_nonfinite_json_constants_refused(self):
|
|
for constant in ['Infinity', '-Infinity', 'NaN']:
|
|
with self.assertRaises(s.Refused):
|
|
s.read_json('{"valid_until_epoch":' + constant + '}')
|
|
self.assertFalse(s.finite_number(s.read_json('{"valid_until_epoch":1e999}')['valid_until_epoch']))
|
|
|
|
def test_hard_timer_covers_slow_trickle_body_and_closes_response(self):
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200')
|
|
class Trickle:
|
|
def getcode(self):
|
|
return 200
|
|
def read(self, size):
|
|
for _ in range(100):
|
|
time.sleep(0.01)
|
|
return b'{}'
|
|
def close(self):
|
|
self.closed = True
|
|
response = Trickle()
|
|
previous = signal.getsignal(signal.SIGALRM)
|
|
started = time.monotonic()
|
|
with patch.object(transport.opener, 'open', return_value=response):
|
|
with self.assertRaisesRegex(s.Refused, 'request_total_timeout'):
|
|
transport.post('https://kc.coulomb.social/token', timeout=0.04)
|
|
self.assertLess(time.monotonic() - started, 0.25)
|
|
self.assertTrue(response.closed)
|
|
self.assertIs(signal.getsignal(signal.SIGALRM), previous)
|
|
self.assertEqual(signal.getitimer(signal.ITIMER_REAL), (0.0, 0.0))
|
|
|
|
def test_hard_timer_spans_headers_and_clips_to_business_window(self):
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200')
|
|
s.REQUEST_DEADLINE = time.time() + 60
|
|
s.BUSINESS_MONOTONIC_DEADLINE = time.monotonic() + 0.04
|
|
def slow_headers(*args, **kwargs):
|
|
time.sleep(1)
|
|
raise AssertionError('inactivity timeout would have permitted this')
|
|
started = time.monotonic()
|
|
with patch.object(transport.opener, 'open', side_effect=slow_headers):
|
|
with self.assertRaisesRegex(s.Refused, 'request_total_timeout'):
|
|
transport.post('https://kc.coulomb.social/token')
|
|
self.assertLess(time.monotonic() - started, 0.25)
|
|
|
|
def test_late_full_acceptance_is_revoked_then_refused(self):
|
|
receipt = {}
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200', receipt=receipt)
|
|
token = 'not-a-real-late-token'
|
|
class Late:
|
|
def getcode(self):
|
|
return 200
|
|
def read(self, size):
|
|
s.REQUEST_DEADLINE = time.time() - 1
|
|
return json.dumps({'auth': {'client_token': token}}).encode()
|
|
def close(self):
|
|
pass
|
|
revoke = type('Response', (), {'getcode':lambda self: 204,
|
|
'read':lambda self, size: b'', 'close':lambda self: None})()
|
|
with patch.object(transport.opener, 'open', side_effect=[Late(), revoke]) as calls:
|
|
with self.assertRaisesRegex(s.Refused, 'approved_window_expired'):
|
|
transport.post('http://openbao.openbao.svc:8200/v1/auth/keycape-services/login')
|
|
self.assertEqual(calls.call_count, 2)
|
|
self.assertEqual(calls.call_args_list[1].args[0].full_url,
|
|
'http://openbao.openbao.svc:8200/v1/auth/token/revoke-self')
|
|
self.assertTrue(receipt['late_acceptance_revoke_attempted'])
|
|
self.assertTrue(receipt['late_acceptance_revoke_succeeded'])
|
|
self.assertNotIn(token, json.dumps(receipt))
|
|
|
|
def test_late_acceptance_failed_revoke_remains_unconfirmed(self):
|
|
receipt = {}
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200', receipt=receipt)
|
|
class Late:
|
|
def getcode(self):
|
|
return 200
|
|
def read(self, size):
|
|
s.REQUEST_DEADLINE = time.time() - 1
|
|
return b'{"auth":{"client_token":"fake-token"}}'
|
|
def close(self):
|
|
pass
|
|
failed = type('Response', (), {'getcode':lambda self: 403,
|
|
'read':lambda self, size: b'{}', 'close':lambda self: None})()
|
|
with patch.object(transport.opener, 'open', side_effect=[Late(), failed]):
|
|
with self.assertRaises(s.Refused):
|
|
transport.post('http://openbao.openbao.svc:8200/v1/auth/keycape-services/login')
|
|
self.assertTrue(receipt['late_acceptance_revoke_attempted'])
|
|
self.assertFalse(receipt['late_acceptance_revoke_succeeded'])
|
|
|
|
def test_partial_accepted_response_timeout_retains_unknown_outcome(self):
|
|
receipt = {}
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200', receipt=receipt)
|
|
response = type('Response', (), {'getcode':lambda self: 200,
|
|
'read':lambda self, size: time.sleep(1), 'close':lambda self: None})()
|
|
with patch.object(transport.opener, 'open', return_value=response):
|
|
with self.assertRaisesRegex(s.Refused, 'request_total_timeout'):
|
|
transport.post('http://openbao.openbao.svc:8200/v1/auth/keycape-services/login', timeout=0.04)
|
|
self.assertTrue(receipt['openbao_login_outcome_unknown'])
|
|
|
|
def test_cleanup_has_finite_aggregate_deadline_and_restoration(self):
|
|
s.REQUEST_DEADLINE = time.time() - 1
|
|
s.begin_cleanup()
|
|
first = s.CLEANUP_MONOTONIC_DEADLINE
|
|
s.begin_cleanup()
|
|
self.assertEqual(s.CLEANUP_MONOTONIC_DEADLINE, first)
|
|
s.CLEANUP_MONOTONIC_DEADLINE = time.monotonic() - 1
|
|
s.CLEANING = True
|
|
with self.assertRaisesRegex(s.Refused, 'cleanup_budget_expired'):
|
|
s.check_deadline()
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200')
|
|
with patch.object(transport.opener, 'open') as backend:
|
|
with self.assertRaises(s.Refused):
|
|
transport.post('http://openbao.openbao.svc:8200/v1/auth/token/revoke-self')
|
|
backend.assert_not_called()
|
|
|
|
def test_expired_cleanup_cannot_issue_native_secret_delete(self):
|
|
s.CLEANING = True
|
|
s.CLEANUP_MONOTONIC_DEADLINE = time.monotonic() - 1
|
|
obj = {'metadata': {'uid':'temporary', 'resourceVersion':'7'}}
|
|
with patch.object(s.subprocess, 'run') as native:
|
|
with self.assertRaisesRegex(s.Refused, 'cleanup_budget_expired'):
|
|
s.delete_temporary(obj)
|
|
native.assert_not_called()
|
|
|
|
def test_secret_delete_total_timeout_clipped_to_cleanup_budget(self):
|
|
s.CLEANING = True
|
|
s.CLEANUP_MONOTONIC_DEADLINE = time.monotonic() + 0.04
|
|
obj = {'metadata': {'uid':'temporary', 'resourceVersion':'7'}}
|
|
captured = {}
|
|
def slow_delete(args, **kwargs):
|
|
captured.update(kwargs)
|
|
time.sleep(1)
|
|
return type('Result', (), {'returncode':0, 'stdout':'{}'})()
|
|
started = time.monotonic()
|
|
with patch.object(s.subprocess, 'run', side_effect=slow_delete):
|
|
with self.assertRaisesRegex(s.Refused, 'request_total_timeout'):
|
|
s.delete_temporary(obj)
|
|
self.assertLess(time.monotonic() - started, 0.25)
|
|
self.assertLessEqual(captured['timeout'], 0.04)
|
|
self.assertEqual(json.loads(captured['input'])['preconditions'], {'uid':'temporary', 'resourceVersion':'7'})
|
|
|
|
def test_early_read_failure_slow_close_is_bounded_once(self):
|
|
receipt = {}
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200', receipt=receipt)
|
|
class Response:
|
|
closes = 0
|
|
def getcode(self):
|
|
return 200
|
|
def read(self, size):
|
|
raise s.Refused('synthetic_read_failure')
|
|
def close(self):
|
|
self.closes += 1
|
|
time.sleep(0.07)
|
|
response = Response()
|
|
previous = signal.getsignal(signal.SIGALRM)
|
|
started = time.monotonic()
|
|
with patch.object(transport.opener, 'open', return_value=response):
|
|
with self.assertRaisesRegex(s.Refused, 'http_response_abort_unconfirmed'):
|
|
transport.post('http://openbao.openbao.svc:8200/v1/auth/keycape-services/login', timeout=0.01)
|
|
self.assertLess(time.monotonic() - started, 0.05)
|
|
self.assertEqual(response.closes, 1)
|
|
self.assertTrue(receipt['openbao_login_outcome_unknown'])
|
|
self.assertTrue(receipt['http_abort_close_attempted'])
|
|
self.assertFalse(receipt['http_abort_close_succeeded'])
|
|
self.assertTrue(receipt['http_response_abort_unconfirmed'])
|
|
self.assertIs(signal.getsignal(signal.SIGALRM), previous)
|
|
self.assertEqual(signal.getitimer(signal.ITIMER_REAL), (0.0, 0.0))
|
|
|
|
def test_total_read_timeout_then_slow_close_uses_only_abort_cap(self):
|
|
receipt = {}
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200', receipt=receipt)
|
|
class Response:
|
|
closes = 0
|
|
def getcode(self):
|
|
return 200
|
|
def read(self, size):
|
|
time.sleep(0.1)
|
|
def close(self):
|
|
self.closes += 1
|
|
time.sleep(0.1)
|
|
response = Response()
|
|
started = time.monotonic()
|
|
with patch.object(transport.opener, 'open', return_value=response):
|
|
with self.assertRaisesRegex(s.Refused, 'http_response_abort_unconfirmed'):
|
|
transport.post('http://openbao.openbao.svc:8200/v1/auth/keycape-services/login', timeout=0.01)
|
|
self.assertLess(time.monotonic() - started, 0.06)
|
|
self.assertEqual(response.closes, 1)
|
|
self.assertTrue(receipt['openbao_login_outcome_unknown'])
|
|
self.assertFalse(receipt['http_abort_close_succeeded'])
|
|
|
|
def test_oversized_body_slow_abort_remains_unproved_and_bounded(self):
|
|
receipt = {}
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200', receipt=receipt)
|
|
response = type('Response', (), {'getcode':lambda self: 200,
|
|
'read':lambda self, size: b'x' * size,
|
|
'close':lambda self: time.sleep(0.1)})()
|
|
started = time.monotonic()
|
|
with patch.object(transport.opener, 'open', return_value=response):
|
|
with self.assertRaisesRegex(s.Refused, 'http_response_abort_unconfirmed'):
|
|
transport.post('http://openbao.openbao.svc:8200/v1/auth/keycape-services/login', timeout=0.01)
|
|
self.assertLess(time.monotonic() - started, 0.05)
|
|
self.assertTrue(receipt['openbao_login_outcome_unknown'])
|
|
self.assertFalse(receipt['http_abort_close_succeeded'])
|
|
|
|
def test_raising_normal_close_then_slow_abort_is_private_bounded_unknown(self):
|
|
receipt = {}
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200', receipt=receipt)
|
|
class Response:
|
|
closes = 0
|
|
def getcode(self):
|
|
return 200
|
|
def read(self, size):
|
|
return b'{}'
|
|
def close(self):
|
|
self.closes += 1
|
|
if self.closes == 1:
|
|
raise RuntimeError('private protected error must not escape')
|
|
time.sleep(0.1)
|
|
response = Response()
|
|
previous = signal.getsignal(signal.SIGALRM)
|
|
started = time.monotonic()
|
|
with patch.object(transport.opener, 'open', return_value=response):
|
|
with self.assertRaisesRegex(s.Refused, '^http_response_abort_unconfirmed$'):
|
|
transport.post('http://openbao.openbao.svc:8200/v1/auth/keycape-services/login', timeout=0.01)
|
|
self.assertLess(time.monotonic() - started, 0.05)
|
|
self.assertEqual(response.closes, 2)
|
|
self.assertTrue(receipt['openbao_login_outcome_unknown'])
|
|
self.assertFalse(receipt['http_abort_close_succeeded'])
|
|
self.assertIs(signal.getsignal(signal.SIGALRM), previous)
|
|
self.assertEqual(signal.getitimer(signal.ITIMER_REAL), (0.0, 0.0))
|
|
|
|
def test_expired_cleanup_never_attempts_exceptional_close_or_resets(self):
|
|
receipt = {}
|
|
transport = s.SessionTransport('http://openbao.openbao.svc:8200', receipt=receipt)
|
|
deadline = time.monotonic() - 1
|
|
s.CLEANUP_MONOTONIC_DEADLINE = deadline
|
|
response = type('Response', (), {'close':lambda self: self.fail('must not close')})()
|
|
with patch.object(response, 'close') as closes:
|
|
with self.assertRaisesRegex(s.Refused, 'http_response_abort_unconfirmed'):
|
|
transport.abort_response(response, time.monotonic() - 1)
|
|
closes.assert_not_called()
|
|
self.assertEqual(s.CLEANUP_MONOTONIC_DEADLINE, deadline)
|
|
self.assertFalse(receipt['http_abort_close_attempted'])
|
|
self.assertTrue(receipt['http_response_abort_unconfirmed'])
|
|
|
|
def test_thread_or_existing_timer_refused_without_overwrite(self):
|
|
results = []
|
|
def in_thread():
|
|
try:
|
|
with s.total_timeout(0.1):
|
|
results.append('unreachable')
|
|
except s.Refused:
|
|
results.append('refused')
|
|
thread = threading.Thread(target=in_thread)
|
|
thread.start()
|
|
thread.join()
|
|
self.assertEqual(results, ['refused'])
|
|
signal.setitimer(signal.ITIMER_REAL, 1)
|
|
try:
|
|
with self.assertRaisesRegex(s.Refused, 'existing_alarm_timer_refused'):
|
|
with s.total_timeout(0.1):
|
|
pass
|
|
self.assertGreater(signal.getitimer(signal.ITIMER_REAL)[0], 0.5)
|
|
finally:
|
|
signal.setitimer(signal.ITIMER_REAL, 0)
|
|
|
|
def test_cleanup_refuses_concurrent_registry_change(self):
|
|
self.lifecycle(drift=True)
|
|
|
|
def test_deleted_client_contract_is_specific(self):
|
|
self.assertTrue(s.removed_client_denied(401, b'{"error":"invalid_profile_usage","feature":"client_id"}'))
|
|
for status, body in [(503, b'{}'), (401, b'{"error":"invalid_profile_usage","feature":"Authorization"}'),
|
|
(401, b'{"error":"invalid_client"}'), (401, b'not-json')]:
|
|
self.assertFalse(s.removed_client_denied(status, body))
|
|
|
|
def test_cleanup_even_when_probe_fails(self):
|
|
self.lifecycle(probe_pass=False)
|
|
|
|
def test_complete_lifecycle(self):
|
|
self.lifecycle()
|
|
|
|
def lifecycle(self, drift=False, probe_pass=True):
|
|
raw = baseline()
|
|
dep_spec = {'replicas': 1, 'strategy': {'type': 'Recreate'}, 'template': {'spec': {'containers': [
|
|
{'name': 'keycape', 'image': 'fixture@sha256:abc', 'env': []}]}}}
|
|
objects = {'keycape-config': {'metadata': {'name': 'keycape-config', 'uid': 'cfg', 'resourceVersion': '1'},
|
|
'data': {'config.yaml': base64.b64encode(raw.encode()).decode(),
|
|
'key.pem': 'FAKE_KEY_NOT_READ'}},
|
|
'keycape': {'metadata': {'name': 'keycape', 'uid': 'dep', 'resourceVersion': '1'},
|
|
'spec': dep_spec}}
|
|
original = copy.deepcopy(objects)
|
|
e = envelope()
|
|
e.update(runner_path='fake-runner', config_metadata=s.meta(objects['keycape-config']),
|
|
deployment_metadata=s.meta(objects['keycape']), deployment_spec_sha256=s.canonical(dep_spec),
|
|
registry_sha256=s.digest(raw.encode()))
|
|
e['source_hashes']['fake-runner'] = 'fixture'
|
|
receipt = {'cleanup_verified': False}
|
|
class Probe:
|
|
def exchange(self, *args):
|
|
return 'fixture-token'
|
|
def http_post(self, *args, **kwargs):
|
|
return 401, b'{"error":"invalid_profile_usage","feature":"client_id"}'
|
|
def run(self, *args, **kwargs):
|
|
if drift:
|
|
objects['keycape-config']['data']['unrelated'] = 'concurrent-owner-change'
|
|
return {'all_refused': probe_pass}
|
|
def kube(args, payload=None):
|
|
if args[0] == 'get':
|
|
return {'metadata': {'uid': s.CLUSTER}}
|
|
if args[0] == 'create':
|
|
if '--dry-run=server' in args:
|
|
return copy.deepcopy(payload)
|
|
created = copy.deepcopy(payload)
|
|
created['metadata'].update(uid='temp', resourceVersion='1')
|
|
objects[s.TEMP] = created
|
|
return created
|
|
if args[0] == 'delete':
|
|
self.assertEqual(payload['preconditions'], {'uid':'temp', 'resourceVersion':'1'})
|
|
del objects[s.TEMP]
|
|
return {'kind':'Status', 'status':'Success'}
|
|
if '--ignore-not-found' in args:
|
|
return objects.get(s.TEMP)
|
|
raise AssertionError('unexpected kube call')
|
|
def patched(kind, obj, path, value, dry=False):
|
|
result = copy.deepcopy(obj)
|
|
if path == '/spec':
|
|
result['spec'] = copy.deepcopy(value)
|
|
else:
|
|
result['data']['config.yaml'] = value
|
|
if not dry:
|
|
objects[obj['metadata']['name']] = result
|
|
return result
|
|
def delete(*args, **kwargs):
|
|
del objects[s.TEMP]
|
|
return type('Result', (), {'returncode': 0})()
|
|
class Discovery:
|
|
def __enter__(self):
|
|
import io
|
|
return io.StringIO('{"issuer":"https://kc.coulomb.social","token_endpoint":"https://kc.coulomb.social/token"}')
|
|
def __exit__(self, *args):
|
|
pass
|
|
with patch.object(s, 'verify_sources', return_value=150), patch.object(s, 'load_probe', return_value=Probe()), \
|
|
patch.object(s, 'SessionTransport') as transport_factory, \
|
|
patch.object(s, 'k', side_effect=kube), patch.object(s, 'get', side_effect=lambda kind, name: copy.deepcopy(objects[name])), \
|
|
patch.object(s, 'patch', side_effect=patched), patch.object(s, 'ready', return_value=True), \
|
|
patch.object(s.subprocess, 'run', side_effect=delete):
|
|
transport_factory.return_value.discovery.return_value = {
|
|
'issuer':'https://kc.coulomb.social', 'token_endpoint':'https://kc.coulomb.social/token'}
|
|
transport_factory.return_value.health_identity.return_value = e['bao_health_identity_sha256']
|
|
transport_factory.return_value.post.side_effect = Probe().http_post
|
|
if drift or not probe_pass:
|
|
with self.assertRaises(s.Refused):
|
|
s.execute(e, {'human_token': 'fake', 'coding_agent_token': 'fake'}, receipt)
|
|
else:
|
|
s.execute(e, {'human_token': 'fake', 'coding_agent_token': 'fake'}, receipt)
|
|
if drift:
|
|
self.assertFalse(receipt['cleanup_verified'])
|
|
self.assertIn(s.TEMP, objects)
|
|
self.assertEqual(objects['keycape-config']['data']['unrelated'], 'concurrent-owner-change')
|
|
else:
|
|
self.assertTrue(receipt['cleanup_verified'])
|
|
self.assertNotIn(s.TEMP, objects)
|
|
self.assertEqual(objects['keycape-config']['data'], original['keycape-config']['data'])
|
|
self.assertEqual(objects['keycape']['spec'], original['keycape']['spec'])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|