From 0856b2d92cf82a5824136515ee61000f63a093e8 Mon Sep 17 00:00:00 2001 From: Sijun Liu Date: Tue, 14 Apr 2020 23:23:45 -0700 Subject: [PATCH 1/2] fix: add mtls system tests and testing cert/key --- .circleci/config.yml | 108 +++++++++++++++++++++++++++++++++++- noxfile.py | 62 +++++++++++++++++++++ tests/cert/mtls.crt | 21 +++++++ tests/cert/mtls.key | 28 ++++++++++ tests/system/conftest.py | 116 ++++++++++++++++++++++++++++++--------- 5 files changed, 307 insertions(+), 28 deletions(-) create mode 100644 tests/cert/mtls.crt create mode 100644 tests/cert/mtls.key diff --git a/.circleci/config.yml b/.circleci/config.yml index adb3b67267..debcecea55 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -91,6 +91,17 @@ workflows: filters: tags: only: /^\d+\.\d+\.\d+$/ + - showcase-mtls: + requires: + - docs + - mypy + - showcase-unit-3.6 + - showcase-unit-3.7 + - showcase-unit-3.8 + - showcase-mypy + filters: + tags: + only: /^\d+\.\d+\.\d+$/ - showcase-alternative-templates: requires: - docs @@ -102,6 +113,17 @@ workflows: filters: tags: only: /^\d+\.\d+\.\d+$/ + - showcase-mtls-alternative-templates: + requires: + - docs + - mypy + - showcase-unit-alternative-templates-3.6 + - showcase-unit-alternative-templates-3.7 + - showcase-unit-alternative-templates-3.8 + - showcase-mypy-alternative-templates + filters: + tags: + only: /^\d+\.\d+\.\d+$/ - docs: filters: tags: @@ -207,7 +229,7 @@ jobs: showcase: docker: - image: python:3.8-slim - - image: gcr.io/gapic-images/gapic-showcase:0.6.1 + - image: gcr.io/gapic-images/gapic-showcase:0.8.1 steps: - checkout - run: @@ -229,10 +251,51 @@ jobs: - run: name: Run showcase tests. command: nox -s showcase + showcase-mtls: + working_directory: /tmp/workspace + docker: + - image: python:3.8-slim + steps: + - checkout + - run: + name: Install system dependencies. + command: | + apt-get update + apt-get install -y curl pandoc unzip + - run: + name: Install nox. + command: pip install nox + - run: + name: Install protoc 3.7.1. + command: | + mkdir -p /usr/src/protoc/ + curl --location https://github.com/google/protobuf/releases/download/v3.7.1/protoc-3.7.1-linux-x86_64.zip --output /usr/src/protoc/protoc-3.7.1.zip + cd /usr/src/protoc/ + unzip protoc-3.7.1.zip + ln -s /usr/src/protoc/bin/protoc /usr/local/bin/protoc + - run: + name: Run showcase tests. + command: | + mkdir gapic_showcase + cd gapic_showcase + curl -sSL https://github.com/googleapis/gapic-showcase/releases/download/v0.8.1/gapic-showcase-0.8.1-linux-amd64.tar.gz | tar xz + ./gapic-showcase run --mtls-ca-cert=/tmp/workspace/tests/cert/mtls.crt --mtls-cert=/tmp/workspace/tests/cert/mtls.crt --mtls-key=/tmp/workspace/tests/cert/mtls.key & + showcase_pid=$! + + cleanup() { + echo "kill showcase server" + kill $showcase_pid + # Wait for the process to die, but don't report error from the kill. + wait $showcase_pid || exit $exit_code + } + trap cleanup EXIT + + cd .. + nox -s showcase_mtls showcase-alternative-templates: docker: - image: python:3.8-slim - - image: gcr.io/gapic-images/gapic-showcase:0.6.1 + - image: gcr.io/gapic-images/gapic-showcase:0.8.1 steps: - checkout - run: @@ -254,6 +317,47 @@ jobs: - run: name: Run showcase tests. command: nox -s showcase_alternative_templates + showcase-mtls-alternative-templates: + working_directory: /tmp/workspace + docker: + - image: python:3.8-slim + steps: + - checkout + - run: + name: Install system dependencies. + command: | + apt-get update + apt-get install -y curl pandoc unzip + - run: + name: Install nox. + command: pip install nox + - run: + name: Install protoc 3.7.1. + command: | + mkdir -p /usr/src/protoc/ + curl --location https://github.com/google/protobuf/releases/download/v3.7.1/protoc-3.7.1-linux-x86_64.zip --output /usr/src/protoc/protoc-3.7.1.zip + cd /usr/src/protoc/ + unzip protoc-3.7.1.zip + ln -s /usr/src/protoc/bin/protoc /usr/local/bin/protoc + - run: + name: Run showcase tests. + command: | + mkdir gapic_showcase + cd gapic_showcase + curl -sSL https://github.com/googleapis/gapic-showcase/releases/download/v0.8.1/gapic-showcase-0.8.1-linux-amd64.tar.gz | tar xz + ./gapic-showcase run --mtls-ca-cert=/tmp/workspace/tests/cert/mtls.crt --mtls-cert=/tmp/workspace/tests/cert/mtls.crt --mtls-key=/tmp/workspace/tests/cert/mtls.key & + showcase_pid=$! + + cleanup() { + echo "kill showcase server" + kill $showcase_pid + # Wait for the process to die, but don't report error from the kill. + wait $showcase_pid || exit $exit_code + } + trap cleanup EXIT + + cd .. + nox -s showcase_mtls_alternative_templates showcase-unit-3.6: docker: - image: python:3.6-slim diff --git a/noxfile.py b/noxfile.py index cfee241ad2..9a6ed566fe 100644 --- a/noxfile.py +++ b/noxfile.py @@ -67,6 +67,7 @@ def showcase( session.log('-' * 70) # Install pytest and gapic-generator-python + session.install('mock') session.install('pytest') session.install('-e', '.') @@ -103,12 +104,73 @@ def showcase( ) +@nox.session(python='3.8') +def showcase_mtls( + session, + templates='DEFAULT', + other_opts: typing.Iterable[str] = (), +): + """Run the Showcase mtls test suite.""" + + # Try to make it clear if Showcase is not running, so that + # people do not end up with tons of difficult-to-debug failures over + # an obvious problem. + if not os.environ.get('CIRCLECI'): + session.log('-' * 70) + session.log('Note: Showcase must be running for these tests to work.') + session.log('See https://github.com/googleapis/gapic-showcase') + session.log('-' * 70) + + # Install pytest and gapic-generator-python + session.install('mock') + session.install('pytest') + session.install('-e', '.') + + # Install a client library for Showcase. + with tempfile.TemporaryDirectory() as tmp_dir: + # Download the Showcase descriptor. + session.run( + 'curl', 'https://github.com/googleapis/gapic-showcase/releases/' + f'download/v{showcase_version}/' + f'gapic-showcase-{showcase_version}.desc', + '-L', '--output', path.join(tmp_dir, 'showcase.desc'), + external=True, + silent=True, + ) + + # Write out a client library for Showcase. + template_opt = f'python-gapic-templates={templates}' + opts = f'--python_gapic_opt={template_opt}' + opts += ','.join(other_opts + ('lazy-import',)) + session.run( + 'protoc', + f'--descriptor_set_in={tmp_dir}{path.sep}showcase.desc', + f'--python_gapic_out={tmp_dir}', + 'google/showcase/v1beta1/echo.proto', + 'google/showcase/v1beta1/identity.proto', + external=True, + ) + + # Install the library. + session.install(tmp_dir) + + session.run( + 'py.test', '--quiet', '--mtls', *(session.posargs or [path.join('tests', 'system')]) + ) + + @nox.session(python='3.8') def showcase_alternative_templates(session): templates = path.join(path.dirname(__file__), 'gapic', 'ads-templates') showcase(session, templates=templates, other_opts=('old-naming',)) +@nox.session(python='3.8') +def showcase_mtls_alternative_templates(session): + templates = path.join(path.dirname(__file__), 'gapic', 'ads-templates') + showcase_mtls(session, templates=templates, other_opts=('old-naming',)) + + @nox.session(python=['3.6', '3.7', '3.8']) def showcase_unit( session, diff --git a/tests/cert/mtls.crt b/tests/cert/mtls.crt new file mode 100644 index 0000000000..f59c43474c --- /dev/null +++ b/tests/cert/mtls.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDbDCCAlSgAwIBAgIJALV2ZblaPmp2MA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQKDAlsb2NhbGhvc3Qx +EjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yMDA0MTUwNjE2NDRaGA8zMDE5MDgxNzA2 +MTY0NFowSjELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNV +BAoMCWxvY2FsaG9zdDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAxOcBZ3f679mn90KA7RzBTr8zwKqcI/7OcJ2GooZh +JvZpD/M6TqhopIgf29O082QrJZLo29lSyVtufTalmg9U4lNDFPAm/BvX7ydaHSdN +FZzn1BInhqvtBXMOy1nGegr4QtgdFSlShuhT8Lo3XxMERP+/Nhyv8wPEy+MTxym3 +WxbJPPhsmQQ42gIgRyqWHVbj6vpCRHp7l81Kh+/wcbC+C/5ARw0vgPIDAAk9iWBU +TJS1q0ghUZyITeafw6fMVqgMAT7vM2WZzfOeOsLunm3t4DQCsJxFrvKQkgi3loXa +MueqepuF0UZIChg/o4k6ecJ2qxD7ad04UsvX1pRBvKKvNQIDAQABo1MwUTAdBgNV +HQ4EFgQUwqm+cCEtQM+Vu05zLforb4IssBswHwYDVR0jBBgwFoAUwqm+cCEtQM+V +u05zLforb4IssBswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA +oqO8ZN92cWNB0TAd9WyPfGz1szn2pRWgOMomEMkry4ESGhOKivrY5CcyMZddfh2a +qmbB0i/pw6/YUVHuhVN369xB/L5pi5UJC+nqdA8p2zSuRidH7cUIxhXTCU6wr8H/ +dZV/tYXmvyRoB7tHh3Jzy1/BhowvCWkBNfAGuFRGb+nlJ2i3Nu9bej32ql4U3zPF +TuOtYH4hSlGa1jBjFp8XM1RiwSA4EkZ79J8Vb0h8IFeMPxobAUiBPLfU+jbmmC90 +aaZI2IhjUUkfUvatLL8brGeo9KdzepaXhQj62OUOyz1ZmAox3TPZNOXgv8+9d8hG +q5TMYoc9yklgNpo+VPtbug== +-----END CERTIFICATE----- diff --git a/tests/cert/mtls.key b/tests/cert/mtls.key new file mode 100644 index 0000000000..a4f01089df --- /dev/null +++ b/tests/cert/mtls.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDE5wFnd/rv2af3 +QoDtHMFOvzPAqpwj/s5wnYaihmEm9mkP8zpOqGikiB/b07TzZCslkujb2VLJW259 +NqWaD1TiU0MU8Cb8G9fvJ1odJ00VnOfUEieGq+0Fcw7LWcZ6CvhC2B0VKVKG6FPw +ujdfEwRE/782HK/zA8TL4xPHKbdbFsk8+GyZBDjaAiBHKpYdVuPq+kJEenuXzUqH +7/BxsL4L/kBHDS+A8gMACT2JYFRMlLWrSCFRnIhN5p/Dp8xWqAwBPu8zZZnN8546 +wu6ebe3gNAKwnEWu8pCSCLeWhdoy56p6m4XRRkgKGD+jiTp5wnarEPtp3ThSy9fW +lEG8oq81AgMBAAECggEAQma4xYDjogEfsLW/rra0xe6a8E1YzJbAXZ/x6Fsy5iXQ +9m0K673FVD8Hp2V0r2PHXSt21bUrQvZPg3BrVlH3ST/U7nmyW/Cz2FXIAO7hAvng +AFeC9tqB2wWbJp6G3V9Xq4sf+6PszcwJirPxumE6Xl50yDXSbDyIIE3avJ5n1BJ6 +S9RrjABVzIQ21/7mjgt8kkz4n7bOuHHkYH24D/NgTjcec/OXU/zcQlHDb3a0MRsG +GjYAWVRM5mg/BJ7Tq0zkibrubWv+Ns2fU9lj3FNVSMpuCqFidwkMcVgreOsdTFLo +GnoZCsHRTsSZNOs5RkFvEKcCzpyAjeNC5aOqpJOYIQKBgQDw0hJe9LhZu2TB0uqr +E1X4a5UdrMolSBSx6hWpSPpSZR4TfVv1nvMG8joiQBt+JJRLUqiMMjLVPvMeM7I7 +XtzgVCcSetIQoyXWOmSBi1aT8m8BJpyWTzTLimiENtPwgFZLOFkiBNuXVjI9cWD6 +9iLHQ766WrBusFJ8VtRpNUghGQKBgQDRUEK24/KFa84PfTTZjJGcJMj9qkNkarPr +KmD1+e9BmRkt/d/ApX6vtQMVS4mfGoQr989VYGkDbQorYo4RMT1fAW/A6tG94nf+ +okUzhWFZZP+kXPtIR11Du9R0XbUbehvp5L72A8ZBLEuR+83N6Ywv3DHcuudYMi1c +1Q2TypF2fQKBgHtzdS9wTEDTq6cgtGPWma+rltmLhmTuOo2p7kjFvG0YoP5kuQuE +3Bine92q920G225xhS25Xg2rI35MCgYdbyUgfpcelykoOXFEpdky0MMu+HCilosH +N41P+pwsbyFO6O3OiQLDcr511XNh99Eu9E0qEU/+xYs4oFBVQrZcNgmhAoGBALqW +58IN1gYmIh0gqlo8uxkMmbe3bjg3/odm0eS0lxpNFmsvY6ViYlrT7Bmxqs8QXj6r +vEIJndOWAnjGdIrS8DifGTZKngq9teZiVXomLq/4HwQwdzjplTRqXmwVlPsXkYah +ibHZj4RNrlhGtiIXTgbkLfbtDopKwLF+o4naDG4pAoGBAMFWBV5EDu6V9lSKUojA +/V1PmbVU5qcaEpE6N6d3M0rk6u486JGwgzn9mcSBktqKJmYXIZwVHJYbJc/v8HNt +rjHp7WkDjQF05QQm7hWjPAN8RXRSbVDUQ9kG/uN6gTbjeH0qqmlFfdBoE26wO97N +Q5o2l+4C3QlHrO5ifRFvh3hX +-----END PRIVATE KEY----- diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 5aa782c94c..60fca41762 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -13,76 +13,140 @@ # limitations under the License. import collections +import mock +import os import pytest +import google.api_core.client_options as ClientOptions +from google import showcase +from google.auth import credentials from google.showcase import EchoClient from google.showcase import IdentityClient import grpc -@pytest.fixture -def echo(): - transport = EchoClient.get_transport_class('grpc')( - channel=grpc.insecure_channel('localhost:7469'), +dir = os.path.dirname(__file__) +with open(os.path.join(dir, "../cert/mtls.crt"), "rb") as fh: + cert = fh.read() +with open(os.path.join(dir, "../cert/mtls.key"), "rb") as fh: + key = fh.read() + +ssl_credentials = grpc.ssl_channel_credentials( + root_certificates=cert, certificate_chain=cert, private_key=key +) + + +def callback(): + return cert, key + + +client_options = ClientOptions.ClientOptions() +client_options.client_cert_source = callback + + +def pytest_addoption(parser): + parser.addoption( + "--mtls", action="store_true", help="Run system test with mutual TLS channel" ) - return EchoClient(transport=transport) @pytest.fixture -def identity(): - transport = IdentityClient.get_transport_class('grpc')( - channel=grpc.insecure_channel('localhost:7469'), - ) - return IdentityClient(transport=transport) +def use_mtls(request): + return request.config.getoption("--mtls") -class MetadataClientInterceptor(grpc.UnaryUnaryClientInterceptor, - grpc.UnaryStreamClientInterceptor, - grpc.StreamUnaryClientInterceptor, - grpc.StreamStreamClientInterceptor): +@pytest.fixture +def echo(use_mtls): + if use_mtls: + with mock.patch("grpc.ssl_channel_credentials", autospec=True) as mock_ssl_cred: + mock_ssl_cred.return_value = ssl_credentials + client = EchoClient( + credentials=credentials.AnonymousCredentials(), + client_options=client_options, + ) + mock_ssl_cred.assert_called_once_with( + certificate_chain=cert, private_key=key + ) + return client + else: + transport = EchoClient.get_transport_class("grpc")( + channel=grpc.insecure_channel("localhost:7469") + ) + return EchoClient(transport=transport) + +@pytest.fixture +def identity(use_mtls): + if use_mtls: + with mock.patch("grpc.ssl_channel_credentials", autospec=True) as mock_ssl_cred: + mock_ssl_cred.return_value = ssl_credentials + client = IdentityClient( + credentials=credentials.AnonymousCredentials(), + client_options=client_options, + ) + mock_ssl_cred.assert_called_once_with( + certificate_chain=cert, private_key=key + ) + return client + else: + transport = IdentityClient.get_transport_class("grpc")( + channel=grpc.insecure_channel("localhost:7469") + ) + return IdentityClient(transport=transport) + + +class MetadataClientInterceptor( + grpc.UnaryUnaryClientInterceptor, + grpc.UnaryStreamClientInterceptor, + grpc.StreamUnaryClientInterceptor, + grpc.StreamStreamClientInterceptor, +): def __init__(self, key, value): self._key = key self._value = value def _add_metadata(self, client_call_details): if client_call_details.metadata is not None: - client_call_details.metadata.append((self._key, self._value,)) + client_call_details.metadata.append((self._key, self._value)) def intercept_unary_unary(self, continuation, client_call_details, request): self._add_metadata(client_call_details) response = continuation(client_call_details, request) return response - def intercept_unary_stream(self, continuation, client_call_details, - request): + def intercept_unary_stream(self, continuation, client_call_details, request): self._add_metadata(client_call_details) response_it = continuation(client_call_details, request) return response_it - def intercept_stream_unary(self, continuation, client_call_details, - request_iterator): + def intercept_stream_unary( + self, continuation, client_call_details, request_iterator + ): self._add_metadata(client_call_details) response = continuation(client_call_details, request_iterator) return response - def intercept_stream_stream(self, continuation, client_call_details, - request_iterator): + def intercept_stream_stream( + self, continuation, client_call_details, request_iterator + ): self._add_metadata(client_call_details) response_it = continuation(client_call_details, request_iterator) return response_it @pytest.fixture -def intercepted_echo(): +def intercepted_echo(use_mtls): # The interceptor adds 'showcase-trailer' client metadata. Showcase server # echos any metadata with key 'showcase-trailer', so the same metadata # should appear as trailing metadata in the response. - interceptor = MetadataClientInterceptor('showcase-trailer', 'intercepted') - channel = grpc.insecure_channel('localhost:7469') + interceptor = MetadataClientInterceptor("showcase-trailer", "intercepted") + if use_mtls: + channel = grpc.secure_channel("localhost:7469", ssl_credentials) + else: + channel = grpc.insecure_channel("localhost:7469") intercept_channel = grpc.intercept_channel(channel, interceptor) - transport = EchoClient.get_transport_class('grpc')( - channel=intercept_channel, + transport = EchoClient.get_transport_class("grpc")( + channel=intercept_channel ) return EchoClient(transport=transport) From fc8cdb14b7c3ac4a32fabde63a4f12387666603d Mon Sep 17 00:00:00 2001 From: Sijun Liu Date: Thu, 16 Apr 2020 16:08:44 -0700 Subject: [PATCH 2/2] update --- tests/system/conftest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 60fca41762..a3108c436d 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -141,10 +141,12 @@ def intercepted_echo(use_mtls): # echos any metadata with key 'showcase-trailer', so the same metadata # should appear as trailing metadata in the response. interceptor = MetadataClientInterceptor("showcase-trailer", "intercepted") - if use_mtls: - channel = grpc.secure_channel("localhost:7469", ssl_credentials) - else: - channel = grpc.insecure_channel("localhost:7469") + host = "localhost:7469" + channel = ( + grpc.secure_channel(host, ssl_credentials) + if use_mtls + else grpc.insecure_channel(host) + ) intercept_channel = grpc.intercept_channel(channel, interceptor) transport = EchoClient.get_transport_class("grpc")( channel=intercept_channel