railiance-platform/tests/test_playwright_consumer.py
codex 6323db59ba Prepare Playwright consumer credentials and identity activation handoff
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13690-09e0-7cd1-964b-ec83cdbcd348
2026-10-13 21:21:58 +02:00

173 lines
8 KiB
Python

"""Credential delivery and evidence checks; synthetic tokens are never login proof."""
import base64
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts/playwright_consumer.py"
spec = importlib.util.spec_from_file_location("playwright_consumer", SCRIPT)
consumer = importlib.util.module_from_spec(spec)
spec.loader.exec_module(consumer)
class ConsumerTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(dir=os.environ.get("PLAYWRIGHT_TEST_TMPDIR", "/tmp"))
self.addCleanup(self.temp.cleanup)
self.directory = Path(self.temp.name)
self.token = self.directory / "token.json"
self.config = self.directory / "kubeconfig.json"
self.claims = dict(iss=consumer.ISSUER, aud=consumer.AUDIENCE,
sub="synthetic", principal_type="human", scope=consumer.SCOPE,
iat=int(time.time()), exp=int(time.time()) + 900)
self.cluster = consumer.configuration(str(self.token))["clusters"][0]["cluster"]
def write_token(self):
part = base64.urlsafe_b64encode(json.dumps(self.claims).encode()).decode().rstrip("=")
value = "synthetic." + part + ".not-a-real-signature"
self.token.write_text(json.dumps(dict(access_token=value, token_type="Bearer")))
self.token.chmod(0o600)
return value
def invoke(self, cluster=None):
env = dict(os.environ, KUBERNETES_EXEC_INFO=json.dumps({
"apiVersion": consumer.API, "kind": "ExecCredential",
"spec": {"cluster": self.cluster if cluster is None else cluster},
}))
return subprocess.run([sys.executable, str(SCRIPT), "credential", "--token-file",
str(self.token)], env=env, capture_output=True, text=True)
def test_exec_delivers_only_expected_credential_and_expiry(self):
token = self.write_token()
result = self.invoke()
self.assertEqual(result.returncode, 0, result.stderr)
value = json.loads(result.stdout)
self.assertEqual(value["status"]["token"], token)
self.assertTrue(value["status"]["expirationTimestamp"].endswith("Z"))
self.assertEqual(result.stderr, "")
def test_registered_coding_agent_supported(self):
self.claims.update(principal_type="service", roles=["coding-agent"])
self.write_token()
self.assertEqual(self.invoke().returncode, 0)
def test_wrong_destination_refused_before_token_read(self):
variants = [dict(server="https://example.org"), {"proxy-url": "http://example.org"},
{"insecure-skip-tls-verify": True}, {"tls-server-name": "other"},
{"certificate-authority-data": "ZmFrZQ=="}]
for change in variants:
with self.subTest(change=change), patch.object(consumer, "read_token") as read:
info = dict(apiVersion=consumer.API, kind="ExecCredential",
spec={"cluster": dict(self.cluster, **change)})
with patch.dict(os.environ, KUBERNETES_EXEC_INFO=json.dumps(info)):
with self.assertRaises(ValueError):
consumer.credential(str(self.token))
read.assert_not_called()
def test_wrong_claims_fail_without_leaking_token(self):
original = self.claims.copy()
variants = [{"iss": "https://other.example"}, {"aud": "openbao"},
{"aud": [consumer.AUDIENCE, "openbao"]}, {"scope": "openid"},
{"principal_type": "service", "roles": []}, {"sub": ""},
{"exp": int(time.time()) - 5}, {"exp": True},
{"iat": int(time.time()) + 100}, {"exp": int(time.time()) + 7200}]
for change in variants:
with self.subTest(change=change):
self.claims = dict(original, **change)
token = self.write_token()
result = self.invoke()
self.assertNotEqual(result.returncode, 0)
self.assertEqual(result.stdout, "")
self.assertNotIn(token, result.stderr)
def test_file_permissions_symlinks_and_hardlinks_refused(self):
self.write_token()
self.token.chmod(0o644)
self.assertNotEqual(self.invoke().returncode, 0)
self.token.chmod(0o600)
other = self.directory / "other.json"
os.link(self.token, other)
self.assertNotEqual(self.invoke().returncode, 0)
self.token.unlink()
self.token.symlink_to(other)
self.assertNotEqual(self.invoke().returncode, 0)
def test_nonprivate_parent_and_git_worktree_refused(self):
self.directory.chmod(0o755)
with self.assertRaises(ValueError):
consumer.configuration(str(self.token))
self.directory.chmod(0o700)
(self.directory / ".git").write_text("gitdir: synthetic")
with self.assertRaises(ValueError):
consumer.configuration(str(self.token))
def test_setup_no_credentials_no_overwrite_and_kubectl_parses(self):
consumer.setup(str(self.token), str(self.config))
config = json.loads(self.config.read_text())
self.assertEqual(self.config.stat().st_mode & 0o777, 0o600)
self.assertEqual(set(config["users"][0]["user"]), {"exec"})
with self.assertRaises(FileExistsError):
consumer.setup(str(self.token), str(self.config))
result = subprocess.run(["kubectl", "--kubeconfig", str(self.config),
"config", "view", "-o", "json"], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout)["current-context"], "playwright")
def test_verify_rejects_impersonation_configuration_without_api_call(self):
config = consumer.configuration(str(self.token))
config["users"][0]["user"]["as"] = "keycape:human:fake"
self.config.write_text(json.dumps(config))
with patch.object(consumer.subprocess, "run") as run:
with self.assertRaises(ValueError):
consumer.verify(str(self.config))
run.assert_not_called()
def test_verify_requires_real_identity_and_all_permission_results(self):
self.config.write_text(json.dumps(consumer.configuration(str(self.token))))
calls = []
def api(argv, **kwargs):
calls.append(argv)
body = json.loads(kwargs["input"])
if body["kind"] == "SelfSubjectReview":
data = {"status": {"userInfo": {"username": "keycape:human:synthetic",
"groups": ["system:authenticated", "playwright-consumers"]}}}
else:
data = {"status": {"allowed": len(calls) <= 4}}
return subprocess.CompletedProcess(argv, 0, json.dumps(data), "")
with patch.object(consumer.subprocess, "run", side_effect=api):
receipt = consumer.verify(str(self.config))
self.assertEqual(len(receipt["checks_passed"]), 9)
self.assertFalse(receipt["impersonated"])
self.assertTrue(all("--kubeconfig" in argv and "--as" not in argv for argv in calls))
with patch.object(consumer.subprocess, "run", return_value=subprocess.CompletedProcess(
[], 0, json.dumps({"status": {"userInfo": {"username": "system:admin",
"groups": ["system:masters"]}}}), "")):
with self.assertRaises(ValueError):
consumer.verify(str(self.config))
calls.clear()
def excessive_permission(argv, **kwargs):
result = api(argv, **kwargs)
if len(calls) > 1:
result.stdout = json.dumps({"status": {"allowed": True}})
return result
with patch.object(consumer.subprocess, "run", side_effect=excessive_permission):
with self.assertRaisesRegex(ValueError, "secrets-denied"):
consumer.verify(str(self.config))
if __name__ == "__main__":
unittest.main()