blob: 3649062e30dbc45b04ad5ba75c644e853cc12b95 [file]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Prevent a tag resolving to an unrelated image from providing runtime evidence."""
import importlib.util
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import subprocess
import tempfile
import threading
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location("images", Path(__file__).with_name("check-toolchain-image.py"))
images = importlib.util.module_from_spec(spec)
spec.loader.exec_module(images)
class RuntimeImageTest(unittest.TestCase):
def test_rejects_container_using_a_different_image(self):
with patch.object(images, "output", return_value="sha256:old"):
with self.assertRaisesRegex(RuntimeError, "expected built image sha256:pr"):
images.verify_image("hubble", "sha256:pr")
def test_rejects_empty_build_result(self):
with patch.object(images, "output", return_value=""):
with self.assertRaises(RuntimeError):
images.verify_image("loader", "")
def test_accepts_built_image(self):
with patch.object(images, "output", return_value="sha256:pr"):
images.verify_image("hubble", "sha256:pr")
def test_requires_application_identity_and_version_not_just_http_success(self):
for body in [None, [], {}, {"status": 200}, {"status": 200, "data": []},
{"status": 200, "data": {"name": "hugegraph-hubble"}},
{"status": 401, "data": {"name": "hugegraph-hubble", "version": "3.0"}},
{"status": 200, "data": {"name": "other", "version": "3.0"}}]:
self.assertFalse(images.valid_hubble_about(body), body)
self.assertTrue(images.valid_hubble_about(
{"status": 200, "data": {"name": "hugegraph-hubble", "version": "3.0.0"}}))
def test_hubble_exit_is_not_readiness_success(self):
with patch.object(images, "output", side_effect=["127.0.0.1:12345", "false"]):
with self.assertRaisesRegex(RuntimeError, "exited before becoming ready"):
images.wait_hubble("hubble")
def test_loader_executes_the_public_script_with_its_permissions_and_shebang(self):
with tempfile.TemporaryDirectory() as temporary:
script = Path(temporary) / "bin/hugegraph-loader.sh"
script.parent.mkdir()
script.write_text('#!/bin/sh\nprintf "Usage: loader --help\\n"\n')
def execute(*args):
self.assertEqual(args[:3], ("docker", "exec", "loader"))
return subprocess.check_output(args[3:], cwd=temporary, text=True)
with patch.object(images, "output", side_effect=execute):
script.chmod(0o755)
images.check_loader("loader")
script.chmod(0o644)
with self.assertRaises(PermissionError):
images.check_loader("loader")
script.chmod(0o755)
script.write_text('#!/missing/interpreter\nprintf "Usage: loader --help\\n"\n')
with self.assertRaises(FileNotFoundError):
images.check_loader("loader")
class HubbleEntryTest(unittest.TestCase):
def setUp(self):
self.routes = {
"/": ("text/html", '<html><div id="root"></div><script src="/main.js"></script>'
'<link rel="stylesheet" href="/main.css"></html>'),
"/main.js": ("application/javascript", 'document.getElementById("root");'),
"/main.css": ("text/css", "body { margin: 0; }"),
}
self.requested = []
routes, requested = self.routes, self.requested
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
requested.append(self.path)
content_type, body = routes.get(self.path, ("text/html", ""))
if content_type == "redirect":
self.send_response(302)
self.send_header("Location", body)
self.end_headers()
return
self.send_response(200 if self.path in routes else 404)
self.send_header("Content-Type", content_type)
self.end_headers()
self.wfile.write(body.encode())
def log_message(self, *args):
pass
self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.thread.start()
self.origin = f"http://127.0.0.1:{self.server.server_port}"
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join()
def test_checks_root_javascript_and_stylesheets(self):
images.check_hubble_ui(self.origin)
self.assertEqual(self.requested, ["/", "/main.js", "/main.css"])
def test_rejects_a_backend_only_page_and_an_unbuilt_template(self):
for page in ['<html>backend ready</html>', '<div id="root"></div>']:
self.routes["/"] = ("text/html", page)
with self.subTest(page=page), self.assertRaises(RuntimeError):
images.check_hubble_ui(self.origin)
def test_rejects_missing_assets_including_spa_html_fallback_and_empty_success(self):
for path in ["/main.js", "/main.css"]:
original = self.routes[path]
for content_type, body in [("text/html", self.routes["/"][1]),
(original[0], " \n"),
(original[0], "<!DOCTYPE html><html>fallback</html>")]:
self.routes[path] = (content_type, body)
with self.subTest(path=path, content_type=content_type, body=body), self.assertRaises(RuntimeError):
images.check_hubble_ui(self.origin)
del self.routes[path]
with self.assertRaises(images.urllib.error.HTTPError):
images.check_hubble_ui(self.origin)
self.routes[path] = original
def test_does_not_fetch_external_scripts(self):
self.routes["/"] = ("text/html", '<div id="root"></div>'
'<script src="https://external.invalid/app.js"></script>')
with self.assertRaisesRegex(RuntimeError, "missing a local JavaScript bundle"):
images.check_hubble_ui(self.origin)
self.assertEqual(self.requested, ["/"])
def test_does_not_follow_asset_redirects_outside_the_container(self):
self.routes["/main.js"] = ("redirect", "https://external.invalid/app.js")
with self.assertRaises(images.urllib.error.HTTPError):
images.check_hubble_ui(self.origin)
self.assertEqual(self.requested, ["/", "/main.js"])