From 8fcd32f442785478fd3f77f2a8f8fa9597a701f0 Mon Sep 17 00:00:00 2001 From: Sony Mathew Date: Wed, 29 Jul 2026 16:42:44 +0530 Subject: [PATCH] chore(search): support Elastic Cloud API keys (#15231) # Pull Request Template ## Description Adds API-key authorization support for Searchkick/OpenSearch so Elastic Cloud deployments can configure advanced search with an Elastic API key instead of embedding basic auth in the URL. The initializer now accepts `OPENSEARCH_API_KEY` or `ELASTICSEARCH_API_KEY` and forwards it as an `Authorization: ApiKey ...` header. `.env.example` also documents the OpenSearch/Elasticsearch-compatible search variables. Refs https://linear.app/chatwoot/issue/CW-7511/populate-test-data-set-and-run-experiments ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [x] This change requires a documentation update ## How Has This Been Tested? - `bundle exec ruby -c config/initializers/searchkick.rb` - `bundle exec ruby -c spec/config/searchkick_spec.rb` - `bundle exec rspec spec/config/searchkick_spec.rb` - `bundle exec rubocop config/initializers/searchkick.rb spec/config/searchkick_spec.rb` - `git diff --check` ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .env.example | 10 ++++++ config/initializers/searchkick.rb | 11 ++++++- spec/config/searchkick_spec.rb | 53 +++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 spec/config/searchkick_spec.rb diff --git a/.env.example b/.env.example index 7445c4ea3..53597282e 100644 --- a/.env.example +++ b/.env.example @@ -59,6 +59,16 @@ REDIS_SENTINEL_MASTER_NAME= # ref: https://github.com/chatwoot/chatwoot/issues/2420 # REDIS_OPENSSL_VERIFY_MODE=none +# OpenSearch / Elasticsearch-compatible search config +# Set OPENSEARCH_URL to enable advanced search. Elastic Cloud API keys can be configured with either key name below. +# API key values must be provided without the ApiKey authorization scheme prefix. +# OPENSEARCH_URL= +# OPENSEARCH_API_KEY= +# ELASTICSEARCH_API_KEY= +# OPENSEARCH_AWS_ACCESS_KEY_ID= +# OPENSEARCH_AWS_SECRET_ACCESS_KEY= +# OPENSEARCH_AWS_REGION=us-east-1 + # Postgres Database config variables # You can leave POSTGRES_DATABASE blank. The default name of # the database in the production environment is chatwoot_production diff --git a/config/initializers/searchkick.rb b/config/initializers/searchkick.rb index e07f9022c..d1356fd78 100644 --- a/config/initializers/searchkick.rb +++ b/config/initializers/searchkick.rb @@ -1,9 +1,18 @@ Searchkick.queue_name = :async_database_migration if ENV.fetch('OPENSEARCH_URL', '').present? +api_key = ENV.fetch('OPENSEARCH_API_KEY', '').presence || ENV.fetch('ELASTICSEARCH_API_KEY', '').presence access_key_id = ENV.fetch('OPENSEARCH_AWS_ACCESS_KEY_ID', '') secret_access_key = ENV.fetch('OPENSEARCH_AWS_SECRET_ACCESS_KEY', '') -if access_key_id.present? && secret_access_key.present? +if api_key.present? + Searchkick.client_options = Searchkick.client_options.deep_merge( + transport_options: { + headers: { + 'Authorization' => "ApiKey #{api_key}" + } + } + ) +elsif access_key_id.present? && secret_access_key.present? region = ENV.fetch('OPENSEARCH_AWS_REGION', 'us-east-1') Searchkick.aws_credentials = { diff --git a/spec/config/searchkick_spec.rb b/spec/config/searchkick_spec.rb new file mode 100644 index 000000000..c4b71d0b1 --- /dev/null +++ b/spec/config/searchkick_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +RSpec.describe Searchkick do + let(:initializer_path) { Rails.root.join('config/initializers/searchkick.rb') } + + around do |example| + original_client_options = described_class.client_options.deep_dup + original_queue_name = described_class.queue_name + original_aws_credentials = described_class.aws_credentials + original_client = described_class.instance_variable_get(:@client) + + example.run + ensure + described_class.client_options = original_client_options + described_class.queue_name = original_queue_name + described_class.instance_variable_set(:@aws_credentials, original_aws_credentials) + described_class.instance_variable_set(:@client, original_client) + end + + it 'configures API key authorization from OPENSEARCH_API_KEY' do + described_class.client_options = {} + + with_modified_env OPENSEARCH_API_KEY: 'opensearch-api-key', ELASTICSEARCH_API_KEY: nil, + OPENSEARCH_AWS_ACCESS_KEY_ID: nil, OPENSEARCH_AWS_SECRET_ACCESS_KEY: nil do + load initializer_path + end + + expect(described_class.client_options).to eq( + transport_options: { + headers: { + 'Authorization' => 'ApiKey opensearch-api-key' + } + } + ) + end + + it 'supports ELASTICSEARCH_API_KEY for Elastic Cloud credentials' do + described_class.client_options = {} + + with_modified_env OPENSEARCH_API_KEY: nil, ELASTICSEARCH_API_KEY: 'elastic-api-key', + OPENSEARCH_AWS_ACCESS_KEY_ID: nil, OPENSEARCH_AWS_SECRET_ACCESS_KEY: nil do + load initializer_path + end + + expect(described_class.client_options).to eq( + transport_options: { + headers: { + 'Authorization' => 'ApiKey elastic-api-key' + } + } + ) + end +end