diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 9e3a53ba15..e7a8e93ae0 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -349,6 +349,28 @@ functions: AUTH=${AUTH} SSL=${SSL} TOPOLOGY=${TOPOLOGY} RVM_RUBY="${RVM_RUBY}" \ .evergreen/run-tests-atlas.sh + "export SFP credentials": + - command: subprocess.exec + type: test + params: + binary: bash + working_dir: "src" + include_expansions_in_env: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, DRIVERS_TOOLS] + args: + - "${DRIVERS_TOOLS}/.evergreen/secrets_handling/setup-secrets.sh" + - "drivers/sfp" + + "run SFP tests": + - command: shell.exec + type: test + params: + shell: bash + working_dir: "src" + script: | + ${PREPARE_SHELL} + RVM_RUBY="${RVM_RUBY}" \ + .evergreen/run-tests-sfp.sh + pre: - func: assume-test-secrets-ec2-role - func: "fetch source" @@ -550,6 +572,10 @@ tasks: commands: - func: "export Atlas credentials" - func: "run Atlas tests" + - name: "test-sfp" + commands: + - func: "export SFP credentials" + - func: "run SFP tests" - name: "test" commands: - func: "run tests" @@ -1726,6 +1752,15 @@ buildvariants: tasks: - name: test-atlas + - matrix_name: "sfp" + matrix_spec: + ruby: ["ruby-4.0", "ruby-3.4", "ruby-3.3", "ruby-3.2", "ruby-3.1"] + os: ubuntu2204 + display_name: "Atlas SFP tests ${ruby}" + tags: ["pr"] + tasks: + - name: test-sfp + # https://jira.mongodb.org/browse/RUBY-3311 # - matrix_name: "aws-lambda" # matrix_spec: diff --git a/.evergreen/config/common.yml.erb b/.evergreen/config/common.yml.erb index cdb72e6803..9d440736d8 100644 --- a/.evergreen/config/common.yml.erb +++ b/.evergreen/config/common.yml.erb @@ -346,6 +346,28 @@ functions: AUTH=${AUTH} SSL=${SSL} TOPOLOGY=${TOPOLOGY} RVM_RUBY="${RVM_RUBY}" \ .evergreen/run-tests-atlas.sh + "export SFP credentials": + - command: subprocess.exec + type: test + params: + binary: bash + working_dir: "src" + include_expansions_in_env: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, DRIVERS_TOOLS] + args: + - "${DRIVERS_TOOLS}/.evergreen/secrets_handling/setup-secrets.sh" + - "drivers/sfp" + + "run SFP tests": + - command: shell.exec + type: test + params: + shell: bash + working_dir: "src" + script: | + ${PREPARE_SHELL} + RVM_RUBY="${RVM_RUBY}" \ + .evergreen/run-tests-sfp.sh + pre: - func: assume-test-secrets-ec2-role - func: "fetch source" @@ -547,6 +569,10 @@ tasks: commands: - func: "export Atlas credentials" - func: "run Atlas tests" + - name: "test-sfp" + commands: + - func: "export SFP credentials" + - func: "run SFP tests" - name: "test" commands: - func: "run tests" diff --git a/.evergreen/config/standard.yml.erb b/.evergreen/config/standard.yml.erb index 984a05cce2..ea1c3d019c 100644 --- a/.evergreen/config/standard.yml.erb +++ b/.evergreen/config/standard.yml.erb @@ -540,6 +540,15 @@ buildvariants: tasks: - name: test-atlas + - matrix_name: "sfp" + matrix_spec: + ruby: <%= supported_mri_rubies_3_ubuntu %> + os: ubuntu2204 + display_name: "Atlas SFP tests ${ruby}" + tags: ["pr"] + tasks: + - name: test-sfp + # https://jira.mongodb.org/browse/RUBY-3311 # - matrix_name: "aws-lambda" # matrix_spec: diff --git a/.evergreen/run-tests-sfp.sh b/.evergreen/run-tests-sfp.sh new file mode 100755 index 0000000000..f4b0808465 --- /dev/null +++ b/.evergreen/run-tests-sfp.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -ex + +. `dirname "$0"`/../spec/shared/shlib/distro.sh +. `dirname "$0"`/../spec/shared/shlib/set_env.sh +. `dirname "$0"`/functions.sh + +set_env_vars +set_env_python + +# Install rbenv and download the requested ruby version +rm -rf ~/.rbenv +git clone https://github.com/rbenv/rbenv.git ~/.rbenv +rm -rf ~/.rbenv/versions/ +curl --retry 3 -fL http://boxes.10gen.com/build/toolchain-drivers/mongo-ruby-toolchain/library/`host_distro`/$RVM_RUBY.tar.xz |tar -xC $HOME/.rbenv/ -Jf - +export PATH="$HOME/.rbenv/bin:$PATH" +eval "$(rbenv init - bash)" +export FULL_RUBY_VERSION=$(ls ~/.rbenv/versions | head -n1) +rbenv global $FULL_RUBY_VERSION + +export JAVA_HOME=/opt/java/jdk21 +export JAVACMD=$JAVA_HOME/bin/java + +bundle_install + +echo "Running specs" + +export SFP_TESTING=1 + +if test -f secrets-export.sh; then + # shellcheck disable=SC1091 + . ./secrets-export.sh +fi + +bundle exec rspec spec/sfp \ + --format Rfc::Riff --format RspecJunitFormatter --out tmp/rspec.xml diff --git a/.mod/drivers-evergreen-tools b/.mod/drivers-evergreen-tools index 9c21ebe36b..b0aded0e55 160000 --- a/.mod/drivers-evergreen-tools +++ b/.mod/drivers-evergreen-tools @@ -1 +1 @@ -Subproject commit 9c21ebe36bf099988360cb83288e4820aa3bf27c +Subproject commit b0aded0e55f747fafa942e2368e77bd128fc5755 diff --git a/spec/sfp/sfp_spec.rb b/spec/sfp/sfp_spec.rb new file mode 100644 index 0000000000..db028649d4 --- /dev/null +++ b/spec/sfp/sfp_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require 'lite_spec_helper' +require 'base64' +require 'tempfile' +require 'uri' + +# Connectivity tests for an Atlas Secure Frontend Processor (SFP), a proxy that +# sits in front of Atlas clusters to provide additional security capabilities. +# See the atlas-sfp-testing specification for the full list of required tests. +# +# These tests run against a preconfigured Atlas cluster and are driven entirely +# by environment variables. They are skipped unless SFP_TESTING is set. +describe 'Atlas SFP connectivity' do + before do + skip 'Set SFP_TESTING to run tests against a live Atlas SFP cluster' unless ENV['SFP_TESTING'] + end + + # Each authenticated test runs under three variations: a baseline, one with a + # compressor enabled, and one with Server API version 1. + variations = { + 'baseline' => {}, + 'with compression' => { compressors: %w[ zlib ] }, + 'with server API version 1' => { server_api: { version: '1' } }, + }.freeze + + # DIAGNOSTIC (RUBY-3800): the SFP proxy closes certless TLS handshakes on + # SFP_ATLAS_URI ("SSL_read: unexpected eof"), while connections presenting the + # X.509 client certificate succeed. Present the certificate at the TLS layer + # for every connection (auth mechanism unchanged) to test whether the proxy + # requires mutual TLS universally. + let(:client_certificate) do + decoded = Base64.strict_decode64(ENV.fetch('SFP_ATLAS_X509_BASE64')) + file = Tempfile.new([ 'sfp-x509-cert', '.pem' ]) + file.write(decoded) + File.chmod(0o600, file.path) + file.close + file + end + + # tlsCertificateKeyFile maps to both :ssl_cert and :ssl_key in the driver. + let(:tls_certificate_options) do + { ssl_cert: client_certificate.path, ssl_key: client_certificate.path } + end + + after do + begin + client.close + rescue StandardError + # no-op + end + + begin + client_certificate.unlink + rescue StandardError + # no-op + end + end + + shared_examples 'connects successfully' do + it 'succeeds at ping' do + result = client.use(:admin).database.command(ping: 1).documents.first + expect(result['ok']).to eq(1) + end + + it 'reports the expected authentication state' do + info = client.use(:admin).database.command(connectionStatus: 1).documents.first + expect(info['ok']).to eq(1) + authenticated_users = info[:authInfo][:authenticatedUsers] + if authenticated + expect(authenticated_users).not_to be_empty + else + expect(authenticated_users).to be_empty + end + end + end + + shared_examples 'performs CRUD operations' do + # Drivers MUST use a unique collection name for each test run. + let(:collection) { client.use('db')["sfp_test_#{BSON::ObjectId.new}"] } + + # Drivers MUST drop the test collection after the test completes, + # regardless of success or failure. + after do + collection.drop + rescue StandardError + # no-op + end + + it 'inserts and reads back a document' do + collection.insert_one(_id: 0) + expect(collection.find(_id: 0).to_a).to eq([ { '_id' => 0 } ]) + end + end + + context 'when unauthenticated' do + let(:authenticated) { false } + let(:client) { Mongo::Client.new(ENV.fetch('SFP_ATLAS_URI'), tls_certificate_options) } + + include_examples 'connects successfully' + end + + context 'when using SCRAM-SHA-256 authentication' do + let(:authenticated) { true } + + variations.each do |description, options| + context description do + let(:client) do + Mongo::Client.new( + ENV.fetch('SFP_ATLAS_URI'), + { + user: ENV.fetch('SFP_ATLAS_USER'), + password: ENV.fetch('SFP_ATLAS_PASSWORD'), + auth_mech: :scram256, + }.merge(tls_certificate_options).merge(options) + ) + end + + include_examples 'connects successfully' + include_examples 'performs CRUD operations' + end + end + end + + context 'when using X.509 authentication' do + let(:authenticated) { true } + + let(:uri) do + "#{ENV.fetch('SFP_ATLAS_X509_URI')}&tlsCertificateKeyFile=" \ + "#{URI::DEFAULT_PARSER.escape(client_certificate.path)}" + end + + variations.each do |description, options| + context description do + let(:client) { Mongo::Client.new(uri, options) } + + include_examples 'connects successfully' + include_examples 'performs CRUD operations' + end + end + end +end