From f1f43ebc2d967e9f17c3b726320ee2f6ee13b2b7 Mon Sep 17 00:00:00 2001 From: hubertdeng123 Date: Tue, 16 Apr 2024 12:21:24 -0700 Subject: [PATCH 1/6] port custom ca cert test to python --- _integration-test/conftest.py | 28 ++-- _integration-test/custom-ca-roots/setup.sh | 46 ------ _integration-test/custom-ca-roots/teardown.sh | 4 - _integration-test/custom-ca-roots/test.py | 5 +- _integration-test/test_backup.py | 47 ++++-- _integration-test/test_run.py | 146 +++++++++++++++--- requirements-dev.txt | 1 + 7 files changed, 179 insertions(+), 98 deletions(-) delete mode 100755 _integration-test/custom-ca-roots/setup.sh delete mode 100755 _integration-test/custom-ca-roots/teardown.sh diff --git a/_integration-test/conftest.py b/_integration-test/conftest.py index cc5ceca3db..a8a229ac77 100644 --- a/_integration-test/conftest.py +++ b/_integration-test/conftest.py @@ -1,6 +1,7 @@ -import subprocess import os +import subprocess import time + import httpx import pytest @@ -10,8 +11,10 @@ TEST_PASS = "test123TEST" TIMEOUT_SECONDS = 60 + def pytest_addoption(parser): - parser.addoption("--customizations", default="disabled") + parser.addoption("--customizations", default="disabled") + @pytest.fixture(scope="session", autouse=True) def configure_self_hosted_environment(request): @@ -28,23 +31,23 @@ def configure_self_hosted_environment(request): raise AssertionError("timeout waiting for self-hosted to come up") if request.config.getoption("--customizations") == "enabled": - os.environ['TEST_CUSTOMIZATIONS'] = "enabled" - script_content = '''\ + os.environ["TEST_CUSTOMIZATIONS"] = "enabled" + script_content = """\ #!/bin/bash touch /created-by-enhance-image apt-get update apt-get install -y gcc libsasl2-dev python-dev libldap2-dev libssl-dev -''' +""" - with open('sentry/enhance-image.sh', 'w') as script_file: + with open("sentry/enhance-image.sh", "w") as script_file: script_file.write(script_content) # Set executable permissions for the shell script - os.chmod('sentry/enhance-image.sh', 0o755) + os.chmod("sentry/enhance-image.sh", 0o755) # Write content to the requirements.txt file - with open('sentry/requirements.txt', 'w') as req_file: - req_file.write('python-ldap\n') - os.environ['MINIMIZE_DOWNTIME'] = "1" + with open("sentry/requirements.txt", "w") as req_file: + req_file.write("python-ldap\n") + os.environ["MINIMIZE_DOWNTIME"] = "1" subprocess.run(["./install.sh"], check=True) # Create test user subprocess.run( @@ -68,7 +71,8 @@ def configure_self_hosted_environment(request): text=True, ) + @pytest.fixture() def setup_backup_restore_env_variables(): - os.environ['SENTRY_DOCKER_IO_DIR'] = os.path.join(os.getcwd(), 'sentry') - os.environ['SKIP_USER_CREATION'] = "1" + os.environ["SENTRY_DOCKER_IO_DIR"] = os.path.join(os.getcwd(), "sentry") + os.environ["SKIP_USER_CREATION"] = "1" diff --git a/_integration-test/custom-ca-roots/setup.sh b/_integration-test/custom-ca-roots/setup.sh deleted file mode 100755 index 19be4feb9f..0000000000 --- a/_integration-test/custom-ca-roots/setup.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -set -e -export COMPOSE_FILE=docker-compose.yml:_integration-test/custom-ca-roots/docker-compose.test.yml - -TEST_NGINX_CONF_PATH=_integration-test/custom-ca-roots/nginx -CUSTOM_CERTS_PATH=certificates - -# generate tightly constrained CA -# NB: `-addext` requires LibreSSL 3.1.0+, or OpenSSL (brew install openssl) -openssl req -x509 -new -nodes -newkey rsa:2048 -keyout $TEST_NGINX_CONF_PATH/ca.key \ - -sha256 -days 1 -out $TEST_NGINX_CONF_PATH/ca.crt -batch \ - -subj "/CN=TEST CA *DO NOT TRUST*" \ - -addext "keyUsage = critical, keyCertSign, cRLSign" \ - -addext "nameConstraints = critical, permitted;DNS:self.test" - -## Lines like the following are debug helpers ... -# openssl x509 -in nginx/ca.crt -text -noout - -mkdir -p $CUSTOM_CERTS_PATH -cp $TEST_NGINX_CONF_PATH/ca.crt $CUSTOM_CERTS_PATH/test-custom-ca-roots.crt - -# generate server certificate -openssl req -new -nodes -newkey rsa:2048 -keyout $TEST_NGINX_CONF_PATH/self.test.key \ - -addext "subjectAltName=DNS:self.test" \ - -out $TEST_NGINX_CONF_PATH/self.test.req -batch -subj "/CN=Self Signed with CA Test Server" - -# openssl req -in nginx/self.test.req -text -noout - -openssl x509 -req -in $TEST_NGINX_CONF_PATH/self.test.req -CA $TEST_NGINX_CONF_PATH/ca.crt -CAkey $TEST_NGINX_CONF_PATH/ca.key \ - -extfile <(printf "subjectAltName=DNS:self.test") \ - -CAcreateserial -out $TEST_NGINX_CONF_PATH/self.test.crt -days 1 -sha256 - -# openssl x509 -in nginx/self.test.crt -text -noout - -# sanity check that signed certificate passes OpenSSL's validation -openssl verify -CAfile $TEST_NGINX_CONF_PATH/ca.crt $TEST_NGINX_CONF_PATH/self.test.crt - -# self signed certificate, for sanity check of not just accepting all certs -openssl req -x509 -newkey rsa:2048 -nodes -days 1 -keyout $TEST_NGINX_CONF_PATH/fake.test.key \ - -out $TEST_NGINX_CONF_PATH/fake.test.crt -addext "subjectAltName=DNS:fake.test" -subj "/CN=Self Signed Test Server" - -# openssl x509 -in nginx/fake.test.crt -text -noout - -cp _integration-test/custom-ca-roots/test.py sentry/test-custom-ca-roots.py - -docker compose --ansi never up -d fixture-custom-ca-roots diff --git a/_integration-test/custom-ca-roots/teardown.sh b/_integration-test/custom-ca-roots/teardown.sh deleted file mode 100755 index 35cee3c7a9..0000000000 --- a/_integration-test/custom-ca-roots/teardown.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -$dc rm -s -f -v fixture-custom-ca-roots -rm -f certificates/test-custom-ca-roots.crt sentry/test-custom-ca-roots.py -unset COMPOSE_FILE diff --git a/_integration-test/custom-ca-roots/test.py b/_integration-test/custom-ca-roots/test.py index 0f9b501f83..bd4fdadbd8 100644 --- a/_integration-test/custom-ca-roots/test.py +++ b/_integration-test/custom-ca-roots/test.py @@ -1,15 +1,16 @@ import unittest + import requests class CustomCATests(unittest.TestCase): def test_valid_self_signed(self): - self.assertEqual(requests.get("https://self.test").text, 'ok') + self.assertEqual(requests.get("https://self.test").text, "ok") def test_invalid_self_signed(self): with self.assertRaises(requests.exceptions.SSLError): requests.get("https://fail.test") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/_integration-test/test_backup.py b/_integration-test/test_backup.py index d125226729..360f598d7d 100644 --- a/_integration-test/test_backup.py +++ b/_integration-test/test_backup.py @@ -1,25 +1,52 @@ -import subprocess import os +import subprocess def test_backup(setup_backup_restore_env_variables): # Docker was giving me permissioning issues when trying to create this file and write to it even after giving read + write access # to group and owner. Instead, try creating the empty file and then give everyone write access to the backup file - file_path = os.path.join(os.getcwd(), 'sentry', 'backup.json') - sentry_admin_sh = os.path.join(os.getcwd(), 'sentry-admin.sh') - open(file_path, 'a', encoding='utf8').close() + file_path = os.path.join(os.getcwd(), "sentry", "backup.json") + sentry_admin_sh = os.path.join(os.getcwd(), "sentry-admin.sh") + open(file_path, "a", encoding="utf8").close() os.chmod(file_path, 0o666) assert os.path.getsize(file_path) == 0 - subprocess.run([sentry_admin_sh, "export", "global", "/sentry-admin/backup.json", "--no-prompt"], check=True) + subprocess.run( + [ + sentry_admin_sh, + "export", + "global", + "/sentry-admin/backup.json", + "--no-prompt", + ], + check=True, + ) assert os.path.getsize(file_path) > 0 + def test_import(setup_backup_restore_env_variables): # Bring postgres down and recreate the docker volume - subprocess.run(["docker", "compose", "--ansi", "never", "stop", "postgres"], check=True) - subprocess.run(["docker", "compose", "--ansi", "never", "rm", "-f", "-v", "postgres"], check=True) + subprocess.run( + ["docker", "compose", "--ansi", "never", "stop", "postgres"], check=True + ) + subprocess.run( + ["docker", "compose", "--ansi", "never", "rm", "-f", "-v", "postgres"], + check=True, + ) subprocess.run(["docker", "volume", "rm", "sentry-postgres"], check=True) subprocess.run(["docker", "volume", "create", "--name=sentry-postgres"], check=True) - subprocess.run(["docker", "compose", "--ansi", "never", "run", "web", "upgrade", "--noinput"], check=True) + subprocess.run( + ["docker", "compose", "--ansi", "never", "run", "web", "upgrade", "--noinput"], + check=True, + ) subprocess.run(["docker", "compose", "--ansi", "never", "up", "-d"], check=True) - sentry_admin_sh = os.path.join(os.getcwd(), 'sentry-admin.sh') - subprocess.run([sentry_admin_sh, "import", "global", "/sentry-admin/backup.json", "--no-prompt"], check=True) + sentry_admin_sh = os.path.join(os.getcwd(), "sentry-admin.sh") + subprocess.run( + [ + sentry_admin_sh, + "import", + "global", + "/sentry-admin/backup.json", + "--no-prompt", + ], + check=True, + ) diff --git a/_integration-test/test_run.py b/_integration-test/test_run.py index d606c521a4..6294d20770 100644 --- a/_integration-test/test_run.py +++ b/_integration-test/test_run.py @@ -1,14 +1,22 @@ -import subprocess +import datetime +import json import os +import re +import shutil +import subprocess +import time from functools import lru_cache -from bs4 import BeautifulSoup +from typing import Callable + import httpx import pytest import sentry_sdk -import time -import json -import re -from typing import Callable +from bs4 import BeautifulSoup +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID SENTRY_CONFIG_PY = "sentry/sentry.conf.py" SENTRY_TEST_HOST = os.getenv("SENTRY_TEST_HOST", "http://localhost:9000") @@ -138,24 +146,114 @@ def test_cleanup_crons_running(): def test_custom_cas(): - try: - subprocess.run(["./_integration-test/custom-ca-roots/setup.sh"], check=True) - subprocess.run( - [ - "docker", - "compose", - "--ansi", - "never", - "run", - "--no-deps", - "web", - "python3", - "/etc/sentry/test-custom-ca-roots.py", - ], - check=True, + # Set environment variable + os.environ["COMPOSE_FILE"] = ( + "docker-compose.yml:_integration-test/custom-ca-roots/docker-compose.test.yml" + ) + + test_nginx_conf_path = "_integration-test/custom-ca-roots/nginx" + custom_certs_path = "certificates" + + # Generate tightly constrained CA + ca_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ) + + ca_name = x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "TEST CA *DO NOT TRUST*")] + ) + + ca_cert = ( + x509.CertificateBuilder() + .subject_name(ca_name) + .issuer_name(ca_name) + .public_key(ca_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) + .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + .add_extension( + x509.KeyUsage( + digital_signature=False, + key_encipherment=False, + content_commitment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=True, + crl_sign=True, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + .add_extension( + x509.NameConstraints([x509.DNSName("self.test")], None), critical=True + ) + .sign(private_key=ca_key, algorithm=hashes.SHA256(), backend=default_backend()) + ) + + ca_key_path = f"{test_nginx_conf_path}/ca.key" + ca_crt_path = f"{test_nginx_conf_path}/ca.crt" + + with open(ca_key_path, "wb") as key_file: + key_file.write( + ca_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) ) - finally: - subprocess.run(["./_integration-test/custom-ca-roots/teardown.sh"], check=True) + + with open(ca_crt_path, "wb") as cert_file: + cert_file.write(ca_cert.public_bytes(serialization.Encoding.PEM)) + + # Create custom certs path and copy ca.crt + os.makedirs(custom_certs_path, exist_ok=True) + shutil.copyfile(ca_crt_path, f"{custom_certs_path}/test-custom-ca-roots.crt") + shutil.copyfile( + "_integration-test/custom-ca-roots/test.py", "sentry/test-custom-ca-roots.py" + ) + + subprocess.run( + ["docker", "compose", "--ansi", "never", "up", "-d", "fixture-custom-ca-roots"], + check=True, + ) + subprocess.run( + [ + "docker", + "compose", + "--ansi", + "never", + "run", + "--no-deps", + "web", + "python3", + "/etc/sentry/test-custom-ca-roots.py", + ], + check=True, + ) + subprocess.run( + [ + "docker", + "compose", + "--ansi", + "never", + "rm", + "-s", + "-f", + "-v", + "fixture-custom-ca-roots", + ], + check=True, + ) + + # Remove files + os.remove(f"{custom_certs_path}/test-custom-ca-roots.crt") + os.remove("sentry/test-custom-ca-roots.py") + + # Unset environment variable + if "COMPOSE_FILE" in os.environ: + del os.environ["COMPOSE_FILE"] def test_receive_transaction_events(client_login): @@ -235,7 +333,7 @@ def test_customizations(): "python", "-c", "import ldap", - ] + ], ] for command in commands: result = subprocess.run(command, check=False) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7ef178da75..9cb58da659 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ +cryptography>=42.0.4 sentry-sdk>=1.39.2 pytest>=8.0.0 pytest-cov>=4.1.0 From 621b949686503f2ee472656d5d05ba170b0aa090 Mon Sep 17 00:00:00 2001 From: hubertdeng123 Date: Tue, 16 Apr 2024 12:34:31 -0700 Subject: [PATCH 2/6] fix --- _integration-test/test_run.py | 150 ++++++++++++++++++++++++++++------ 1 file changed, 126 insertions(+), 24 deletions(-) diff --git a/_integration-test/test_run.py b/_integration-test/test_run.py index 6294d20770..24e9fa981c 100644 --- a/_integration-test/test_run.py +++ b/_integration-test/test_run.py @@ -210,10 +210,112 @@ def test_custom_cas(): # Create custom certs path and copy ca.crt os.makedirs(custom_certs_path, exist_ok=True) shutil.copyfile(ca_crt_path, f"{custom_certs_path}/test-custom-ca-roots.crt") - shutil.copyfile( - "_integration-test/custom-ca-roots/test.py", "sentry/test-custom-ca-roots.py" + # Generate server key and certificate + + self_test_key_path = os.path.join(test_nginx_conf_path, "self.test.key") + self_test_csr_path = os.path.join(test_nginx_conf_path, "self.test.csr") + self_test_cert_path = os.path.join(test_nginx_conf_path, "self.test.crt") + + self_test_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + self_test_req = ( + x509.CertificateSigningRequestBuilder() + .subject_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.COMMON_NAME, "Self Signed with CA Test Server" + ) + ] + ) + ) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("self.test")]), critical=False + ) + .sign(self_test_key, hashes.SHA256()) + ) + + self_test_cert = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.COMMON_NAME, "Self Signed with CA Test Server" + ) + ] + ) + ) + .issuer_name(ca_cert.issuer) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) + .public_key(self_test_req.public_key()) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("self.test")]), critical=False + ) + .sign(private_key=ca_key, algorithm=hashes.SHA256()) ) + # Save server key, CSR, and certificate + with open(self_test_key_path, "wb") as key_file: + key_file.write( + self_test_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + with open(self_test_csr_path, "wb") as csr_file: + csr_file.write(self_test_req.public_bytes(serialization.Encoding.PEM)) + with open(self_test_cert_path, "wb") as cert_file: + cert_file.write(self_test_cert.public_bytes(serialization.Encoding.PEM)) + + # Generate server key and certificate for fake.test + + fake_test_key_path = os.path.join(test_nginx_conf_path, "fake.test.key") + fake_test_cert_path = os.path.join(test_nginx_conf_path, "fake.test.crt") + + fake_test_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + fake_test_cert = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "Self Signed Test Server")] + ) + ) + .issuer_name( + x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "Self Signed Test Server")] + ) + ) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) + .public_key(fake_test_key.public_key()) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("fake.test")]), critical=False + ) + .sign(private_key=fake_test_key, algorithm=hashes.SHA256()) + ) + + # Save server key and certificate for fake.test + with open(fake_test_key_path, "wb") as key_file: + key_file.write( + fake_test_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + with open(fake_test_cert_path, "wb") as cert_file: + cert_file.write(fake_test_cert.public_bytes(serialization.Encoding.PEM)) + shutil.copyfile( + "_integration-test/custom-ca-roots/test.py", + "sentry/test-custom-ca-roots.py", + ) + subprocess.run( ["docker", "compose", "--ansi", "never", "up", "-d", "fixture-custom-ca-roots"], check=True, @@ -232,28 +334,28 @@ def test_custom_cas(): ], check=True, ) - subprocess.run( - [ - "docker", - "compose", - "--ansi", - "never", - "rm", - "-s", - "-f", - "-v", - "fixture-custom-ca-roots", - ], - check=True, - ) - - # Remove files - os.remove(f"{custom_certs_path}/test-custom-ca-roots.crt") - os.remove("sentry/test-custom-ca-roots.py") - - # Unset environment variable - if "COMPOSE_FILE" in os.environ: - del os.environ["COMPOSE_FILE"] + # subprocess.run( + # [ + # "docker", + # "compose", + # "--ansi", + # "never", + # "rm", + # "-s", + # "-f", + # "-v", + # "fixture-custom-ca-roots", + # ], + # check=True, + # ) + + # # Remove files + # os.remove(f"{custom_certs_path}/test-custom-ca-roots.crt") + # os.remove("sentry/test-custom-ca-roots.py") + + # # Unset environment variable + # if "COMPOSE_FILE" in os.environ: + # del os.environ["COMPOSE_FILE"] def test_receive_transaction_events(client_login): From 2673f2592f53ab69ffbb5c2fdf0d9fbe3e86f8a6 Mon Sep 17 00:00:00 2001 From: hubertdeng123 Date: Tue, 16 Apr 2024 12:45:56 -0700 Subject: [PATCH 3/6] just use openssl --- _integration-test/test_run.py | 304 ++++++++++++++-------------------- requirements-dev.txt | 1 - 2 files changed, 125 insertions(+), 180 deletions(-) diff --git a/_integration-test/test_run.py b/_integration-test/test_run.py index 24e9fa981c..1a9396e0ba 100644 --- a/_integration-test/test_run.py +++ b/_integration-test/test_run.py @@ -1,8 +1,6 @@ -import datetime import json import os import re -import shutil import subprocess import time from functools import lru_cache @@ -12,11 +10,6 @@ import pytest import sentry_sdk from bs4 import BeautifulSoup -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.x509.oid import NameOID SENTRY_CONFIG_PY = "sentry/sentry.conf.py" SENTRY_TEST_HOST = os.getenv("SENTRY_TEST_HOST", "http://localhost:9000") @@ -146,7 +139,7 @@ def test_cleanup_crons_running(): def test_custom_cas(): - # Set environment variable + # Set environment variables os.environ["COMPOSE_FILE"] = ( "docker-compose.yml:_integration-test/custom-ca-roots/docker-compose.test.yml" ) @@ -155,167 +148,120 @@ def test_custom_cas(): custom_certs_path = "certificates" # Generate tightly constrained CA - ca_key = rsa.generate_private_key( - public_exponent=65537, key_size=2048, backend=default_backend() - ) - - ca_name = x509.Name( - [x509.NameAttribute(NameOID.COMMON_NAME, "TEST CA *DO NOT TRUST*")] - ) - - ca_cert = ( - x509.CertificateBuilder() - .subject_name(ca_name) - .issuer_name(ca_name) - .public_key(ca_key.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.datetime.utcnow()) - .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) - .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) - .add_extension( - x509.KeyUsage( - digital_signature=False, - key_encipherment=False, - content_commitment=False, - data_encipherment=False, - key_agreement=False, - key_cert_sign=True, - crl_sign=True, - encipher_only=False, - decipher_only=False, - ), - critical=True, - ) - .add_extension( - x509.NameConstraints([x509.DNSName("self.test")], None), critical=True - ) - .sign(private_key=ca_key, algorithm=hashes.SHA256(), backend=default_backend()) - ) - ca_key_path = f"{test_nginx_conf_path}/ca.key" ca_crt_path = f"{test_nginx_conf_path}/ca.crt" - - with open(ca_key_path, "wb") as key_file: - key_file.write( - ca_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - ) - - with open(ca_crt_path, "wb") as cert_file: - cert_file.write(ca_cert.public_bytes(serialization.Encoding.PEM)) + subprocess.run( + [ + "openssl", + "req", + "-x509", + "-new", + "-nodes", + "-newkey", + "rsa:2048", + "-keyout", + ca_key_path, + "-sha256", + "-days", + "1", + "-out", + ca_crt_path, + "-batch", + "-subj", + "/CN=TEST CA *DO NOT TRUST*", + "-addext", + "keyUsage = critical, keyCertSign, cRLSign", + "-addext", + "nameConstraints = critical, permitted;DNS:self.test", + ], + check=True, + ) # Create custom certs path and copy ca.crt os.makedirs(custom_certs_path, exist_ok=True) - shutil.copyfile(ca_crt_path, f"{custom_certs_path}/test-custom-ca-roots.crt") - # Generate server key and certificate - - self_test_key_path = os.path.join(test_nginx_conf_path, "self.test.key") - self_test_csr_path = os.path.join(test_nginx_conf_path, "self.test.csr") - self_test_cert_path = os.path.join(test_nginx_conf_path, "self.test.crt") - - self_test_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - - self_test_req = ( - x509.CertificateSigningRequestBuilder() - .subject_name( - x509.Name( - [ - x509.NameAttribute( - NameOID.COMMON_NAME, "Self Signed with CA Test Server" - ) - ] - ) - ) - .add_extension( - x509.SubjectAlternativeName([x509.DNSName("self.test")]), critical=False - ) - .sign(self_test_key, hashes.SHA256()) + subprocess.run( + ["cp", ca_crt_path, f"{custom_certs_path}/test-custom-ca-roots.crt"], check=True ) - self_test_cert = ( - x509.CertificateBuilder() - .subject_name( - x509.Name( - [ - x509.NameAttribute( - NameOID.COMMON_NAME, "Self Signed with CA Test Server" - ) - ] - ) - ) - .issuer_name(ca_cert.issuer) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.datetime.utcnow()) - .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) - .public_key(self_test_req.public_key()) - .add_extension( - x509.SubjectAlternativeName([x509.DNSName("self.test")]), critical=False - ) - .sign(private_key=ca_key, algorithm=hashes.SHA256()) + # Generate server certificate + self_test_key_path = f"{test_nginx_conf_path}/self.test.key" + self_test_req_path = f"{test_nginx_conf_path}/self.test.req" + subprocess.run( + [ + "openssl", + "req", + "-new", + "-nodes", + "-newkey", + "rsa:2048", + "-keyout", + self_test_key_path, + "-addext", + "subjectAltName=DNS:self.test", + "-out", + self_test_req_path, + "-batch", + "-subj", + "/CN=Self Signed with CA Test Server", + ], + check=True, ) - # Save server key, CSR, and certificate - with open(self_test_key_path, "wb") as key_file: - key_file.write( - self_test_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - ) - with open(self_test_csr_path, "wb") as csr_file: - csr_file.write(self_test_req.public_bytes(serialization.Encoding.PEM)) - with open(self_test_cert_path, "wb") as cert_file: - cert_file.write(self_test_cert.public_bytes(serialization.Encoding.PEM)) - - # Generate server key and certificate for fake.test - - fake_test_key_path = os.path.join(test_nginx_conf_path, "fake.test.key") - fake_test_cert_path = os.path.join(test_nginx_conf_path, "fake.test.crt") - - fake_test_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - - fake_test_cert = ( - x509.CertificateBuilder() - .subject_name( - x509.Name( - [x509.NameAttribute(NameOID.COMMON_NAME, "Self Signed Test Server")] - ) - ) - .issuer_name( - x509.Name( - [x509.NameAttribute(NameOID.COMMON_NAME, "Self Signed Test Server")] - ) - ) - .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.datetime.utcnow()) - .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) - .public_key(fake_test_key.public_key()) - .add_extension( - x509.SubjectAlternativeName([x509.DNSName("fake.test")]), critical=False - ) - .sign(private_key=fake_test_key, algorithm=hashes.SHA256()) + # Create self-signed certificate using the CA + self_test_crt_path = f"{test_nginx_conf_path}/self.test.crt" + subprocess.run( + [ + "openssl", + "x509", + "-req", + "-in", + self_test_req_path, + "-CA", + ca_crt_path, + "-CAkey", + ca_key_path, + "-extfile", + '<(printf "subjectAltName=DNS:self.test")', + "-CAcreateserial", + "-out", + self_test_crt_path, + "-days", + "1", + "-sha256", + ], + shell=True, + check=True, ) - # Save server key and certificate for fake.test - with open(fake_test_key_path, "wb") as key_file: - key_file.write( - fake_test_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - ) - with open(fake_test_cert_path, "wb") as cert_file: - cert_file.write(fake_test_cert.public_bytes(serialization.Encoding.PEM)) - shutil.copyfile( - "_integration-test/custom-ca-roots/test.py", - "sentry/test-custom-ca-roots.py", - ) + # Sanity check that signed certificate passes OpenSSL's validation + subprocess.run( + ["openssl", "verify", "-CAfile", ca_crt_path, self_test_crt_path], check=True + ) + # Create self-signed certificate with a different subjectAltName + fake_test_key_path = f"{test_nginx_conf_path}/fake.test.key" + fake_test_crt_path = f"{test_nginx_conf_path}/fake.test.crt" + subprocess.run( + [ + "openssl", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-days", + "1", + "-keyout", + fake_test_key_path, + "-out", + fake_test_crt_path, + "-addext", + "subjectAltName=DNS:fake.test", + "-subj", + "/CN=Self Signed Test Server", + ], + check=True, + ) subprocess.run( ["docker", "compose", "--ansi", "never", "up", "-d", "fixture-custom-ca-roots"], check=True, @@ -334,28 +280,28 @@ def test_custom_cas(): ], check=True, ) - # subprocess.run( - # [ - # "docker", - # "compose", - # "--ansi", - # "never", - # "rm", - # "-s", - # "-f", - # "-v", - # "fixture-custom-ca-roots", - # ], - # check=True, - # ) - - # # Remove files - # os.remove(f"{custom_certs_path}/test-custom-ca-roots.crt") - # os.remove("sentry/test-custom-ca-roots.py") - - # # Unset environment variable - # if "COMPOSE_FILE" in os.environ: - # del os.environ["COMPOSE_FILE"] + subprocess.run( + [ + "docker", + "compose", + "--ansi", + "never", + "rm", + "-s", + "-f", + "-v", + "fixture-custom-ca-roots", + ], + check=True, + ) + + # Remove files + os.remove(f"{custom_certs_path}/test-custom-ca-roots.crt") + os.remove("sentry/test-custom-ca-roots.py") + + # Unset environment variable + if "COMPOSE_FILE" in os.environ: + del os.environ["COMPOSE_FILE"] def test_receive_transaction_events(client_login): diff --git a/requirements-dev.txt b/requirements-dev.txt index 9cb58da659..7ef178da75 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,3 @@ -cryptography>=42.0.4 sentry-sdk>=1.39.2 pytest>=8.0.0 pytest-cov>=4.1.0 From bb597910ce5407c53bf76722271e9a3dbd4987d9 Mon Sep 17 00:00:00 2001 From: hubertdeng123 Date: Tue, 16 Apr 2024 12:53:33 -0700 Subject: [PATCH 4/6] attempt #2 --- _integration-test/test_run.py | 260 ++++++++++++++++++++-------------- 1 file changed, 157 insertions(+), 103 deletions(-) diff --git a/_integration-test/test_run.py b/_integration-test/test_run.py index 1a9396e0ba..5d8f805449 100644 --- a/_integration-test/test_run.py +++ b/_integration-test/test_run.py @@ -1,6 +1,8 @@ +import datetime import json import os import re +import shutil import subprocess import time from functools import lru_cache @@ -10,6 +12,11 @@ import pytest import sentry_sdk from bs4 import BeautifulSoup +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID SENTRY_CONFIG_PY = "sentry/sentry.conf.py" SENTRY_TEST_HOST = os.getenv("SENTRY_TEST_HOST", "http://localhost:9000") @@ -139,7 +146,7 @@ def test_cleanup_crons_running(): def test_custom_cas(): - # Set environment variables + # Set environment variable os.environ["COMPOSE_FILE"] = ( "docker-compose.yml:_integration-test/custom-ca-roots/docker-compose.test.yml" ) @@ -148,120 +155,167 @@ def test_custom_cas(): custom_certs_path = "certificates" # Generate tightly constrained CA + ca_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ) + + ca_name = x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "TEST CA *DO NOT TRUST*")] + ) + + ca_cert = ( + x509.CertificateBuilder() + .subject_name(ca_name) + .issuer_name(ca_name) + .public_key(ca_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) + .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + .add_extension( + x509.KeyUsage( + digital_signature=False, + key_encipherment=False, + content_commitment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=True, + crl_sign=True, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + .add_extension( + x509.NameConstraints([x509.DNSName("self.test")], None), critical=True + ) + .sign(private_key=ca_key, algorithm=hashes.SHA256(), backend=default_backend()) + ) + ca_key_path = f"{test_nginx_conf_path}/ca.key" ca_crt_path = f"{test_nginx_conf_path}/ca.crt" - subprocess.run( - [ - "openssl", - "req", - "-x509", - "-new", - "-nodes", - "-newkey", - "rsa:2048", - "-keyout", - ca_key_path, - "-sha256", - "-days", - "1", - "-out", - ca_crt_path, - "-batch", - "-subj", - "/CN=TEST CA *DO NOT TRUST*", - "-addext", - "keyUsage = critical, keyCertSign, cRLSign", - "-addext", - "nameConstraints = critical, permitted;DNS:self.test", - ], - check=True, - ) + + with open(ca_key_path, "wb") as key_file: + key_file.write( + ca_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + + with open(ca_crt_path, "wb") as cert_file: + cert_file.write(ca_cert.public_bytes(serialization.Encoding.PEM)) # Create custom certs path and copy ca.crt os.makedirs(custom_certs_path, exist_ok=True) - subprocess.run( - ["cp", ca_crt_path, f"{custom_certs_path}/test-custom-ca-roots.crt"], check=True + shutil.copyfile(ca_crt_path, f"{custom_certs_path}/test-custom-ca-roots.crt") + # Generate server key and certificate + + self_test_key_path = os.path.join(test_nginx_conf_path, "self.test.key") + self_test_csr_path = os.path.join(test_nginx_conf_path, "self.test.csr") + self_test_cert_path = os.path.join(test_nginx_conf_path, "self.test.crt") + + self_test_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + self_test_req = ( + x509.CertificateSigningRequestBuilder() + .subject_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.COMMON_NAME, "Self Signed with CA Test Server" + ) + ] + ) + ) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("self.test")]), critical=False + ) + .sign(self_test_key, hashes.SHA256()) ) - # Generate server certificate - self_test_key_path = f"{test_nginx_conf_path}/self.test.key" - self_test_req_path = f"{test_nginx_conf_path}/self.test.req" - subprocess.run( - [ - "openssl", - "req", - "-new", - "-nodes", - "-newkey", - "rsa:2048", - "-keyout", - self_test_key_path, - "-addext", - "subjectAltName=DNS:self.test", - "-out", - self_test_req_path, - "-batch", - "-subj", - "/CN=Self Signed with CA Test Server", - ], - check=True, + self_test_cert = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.COMMON_NAME, "Self Signed with CA Test Server" + ) + ] + ) + ) + .issuer_name(ca_cert.issuer) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) + .public_key(self_test_req.public_key()) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("self.test")]), critical=False + ) + .sign(private_key=ca_key, algorithm=hashes.SHA256()) ) - # Create self-signed certificate using the CA - self_test_crt_path = f"{test_nginx_conf_path}/self.test.crt" - subprocess.run( - [ - "openssl", - "x509", - "-req", - "-in", - self_test_req_path, - "-CA", - ca_crt_path, - "-CAkey", - ca_key_path, - "-extfile", - '<(printf "subjectAltName=DNS:self.test")', - "-CAcreateserial", - "-out", - self_test_crt_path, - "-days", - "1", - "-sha256", - ], - shell=True, - check=True, - ) + # Save server key, CSR, and certificate + with open(self_test_key_path, "wb") as key_file: + key_file.write( + self_test_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + with open(self_test_csr_path, "wb") as csr_file: + csr_file.write(self_test_req.public_bytes(serialization.Encoding.PEM)) + with open(self_test_cert_path, "wb") as cert_file: + cert_file.write(self_test_cert.public_bytes(serialization.Encoding.PEM)) - # Sanity check that signed certificate passes OpenSSL's validation - subprocess.run( - ["openssl", "verify", "-CAfile", ca_crt_path, self_test_crt_path], check=True - ) + # Generate server key and certificate for fake.test - # Create self-signed certificate with a different subjectAltName - fake_test_key_path = f"{test_nginx_conf_path}/fake.test.key" - fake_test_crt_path = f"{test_nginx_conf_path}/fake.test.crt" - subprocess.run( - [ - "openssl", - "req", - "-x509", - "-newkey", - "rsa:2048", - "-nodes", - "-days", - "1", - "-keyout", - fake_test_key_path, - "-out", - fake_test_crt_path, - "-addext", - "subjectAltName=DNS:fake.test", - "-subj", - "/CN=Self Signed Test Server", - ], - check=True, + fake_test_key_path = os.path.join(test_nginx_conf_path, "fake.test.key") + fake_test_cert_path = os.path.join(test_nginx_conf_path, "fake.test.crt") + + fake_test_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + fake_test_cert = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "Self Signed Test Server")] + ) + ) + .issuer_name( + x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "Self Signed Test Server")] + ) + ) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) + .public_key(fake_test_key.public_key()) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("fake.test")]), critical=False + ) + .sign(private_key=fake_test_key, algorithm=hashes.SHA256()) ) + + # Save server key and certificate for fake.test + with open(fake_test_key_path, "wb") as key_file: + key_file.write( + fake_test_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + with open(fake_test_cert_path, "wb") as cert_file: + cert_file.write(fake_test_cert.public_bytes(serialization.Encoding.PEM)) + shutil.copyfile( + "_integration-test/custom-ca-roots/test.py", + "sentry/test-custom-ca-roots.py", + ) + subprocess.run( ["docker", "compose", "--ansi", "never", "up", "-d", "fixture-custom-ca-roots"], check=True, From b8bdd02ab3166263e6b6a018dc94fc55a16b21df Mon Sep 17 00:00:00 2001 From: hubertdeng123 Date: Tue, 16 Apr 2024 14:26:50 -0700 Subject: [PATCH 5/6] add sentry admin tests --- .../ensure-sentry-admin-works.sh | 27 ------------------- _integration-test/test_backup.py | 17 ++++++++++++ sentry-admin.sh | 3 ++- 3 files changed, 19 insertions(+), 28 deletions(-) delete mode 100644 _integration-test/ensure-sentry-admin-works.sh diff --git a/_integration-test/ensure-sentry-admin-works.sh b/_integration-test/ensure-sentry-admin-works.sh deleted file mode 100644 index f8f574ec8a..0000000000 --- a/_integration-test/ensure-sentry-admin-works.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -set -ex - -source install/_lib.sh -source install/dc-detect-version.sh - -echo "${_group}Test that sentry-admin works..." - -echo "Global help documentation..." - -global_help_doc=$(/bin/bash --help) -if ! echo "$global_help_doc" | grep -q "^Usage: ./sentry-admin.sh"; then - echo "Assertion failed: Incorrect binary name in global help docs" - exit 1 -fi -if ! echo "$global_help_doc" | grep -q "SENTRY_DOCKER_IO_DIR"; then - echo "Assertion failed: Missing SENTRY_DOCKER_IO_DIR global help doc" - exit 1 -fi - -echo "Command-specific help documentation..." - -command_help_doc=$(/bin/bash permissions --help) -if ! echo "$command_help_doc" | grep -q "^Usage: ./sentry-admin.sh permissions"; then - echo "Assertion failed: Incorrect binary name in command-specific help docs" - exit 1 -fi diff --git a/_integration-test/test_backup.py b/_integration-test/test_backup.py index 360f598d7d..8c482869e8 100644 --- a/_integration-test/test_backup.py +++ b/_integration-test/test_backup.py @@ -2,6 +2,23 @@ import subprocess +def test_sentry_admin(setup_backup_restore_env_variables): + sentry_admin_sh = os.path.join(os.getcwd(), "sentry-admin.sh") + output = subprocess.run( + [sentry_admin_sh, "--help"], check=True, capture_output=True, encoding="utf8" + ).stdout + assert "Usage: ./sentry-admin.sh" in output + assert "SENTRY_DOCKER_IO_DIR" in output + + output = subprocess.run( + [sentry_admin_sh, "permissions", "--help"], + check=True, + capture_output=True, + encoding="utf8", + ).stdout + assert "Usage: ./sentry-admin.sh permissions" in output + + def test_backup(setup_backup_restore_env_variables): # Docker was giving me permissioning issues when trying to create this file and write to it even after giving read + write access # to group and owner. Instead, try creating the empty file and then give everyone write access to the backup file diff --git a/sentry-admin.sh b/sentry-admin.sh index ff50d9f475..85705516d2 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -19,7 +19,8 @@ on the host filesystem. Commands that write files should write them to the '/sen # Actual invocation that runs the command in the container. invocation() { - $dc run -v "$VOLUME_MAPPING" --rm -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" + output=$($dc run -v "$VOLUME_MAPPING" --rm -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1) + echo "$output" } # Function to modify lines starting with `Usage: sentry` to say `Usage: ./sentry-admin.sh` instead. From 2c7fd2a1e05aff393cf06c3316abf536033e955e Mon Sep 17 00:00:00 2001 From: hubertdeng123 Date: Tue, 16 Apr 2024 14:39:09 -0700 Subject: [PATCH 6/6] add note of asserting using test script that is mounted in sentry directory --- _integration-test/test_run.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_integration-test/test_run.py b/_integration-test/test_run.py index 5d8f805449..84bc027c2b 100644 --- a/_integration-test/test_run.py +++ b/_integration-test/test_run.py @@ -145,7 +145,7 @@ def test_cleanup_crons_running(): assert len(cleanup_crons) > 0 -def test_custom_cas(): +def test_custom_certificate_authorities(): # Set environment variable os.environ["COMPOSE_FILE"] = ( "docker-compose.yml:_integration-test/custom-ca-roots/docker-compose.test.yml" @@ -309,6 +309,7 @@ def test_custom_cas(): encryption_algorithm=serialization.NoEncryption(), ) ) + # Our asserts for this test case must be executed within the web container, so we are copying a python test script into the mounted sentry directory with open(fake_test_cert_path, "wb") as cert_file: cert_file.write(fake_test_cert.public_bytes(serialization.Encoding.PEM)) shutil.copyfile(