Skip to content

Commit 5637a95

Browse files
authored
Add configurable HTTP auth modes for ClickHouse requests (#240)
* Add configurable HTTP auth modes Keeps the current behavior as default, sending user and password as URL parameters. Accept http_auth config with x_clickhouse_headers or basic options - basic` sends `Authorization: Basic ...` and keeps `database` in URL params. - x_clickhouse_headers` sends `X-ClickHouse-User`, `X-ClickHouse-Key`, and `X-ClickHouse-Database` headers. * Add explicit query_params http_auth mode It documents and validates all supported modes to reduce ambiguity * Improve local single-spec workflow Fix console loading and add helper/docs to run single adapter tests consistently
1 parent c63ccb4 commit 5637a95

File tree

6 files changed

+403
-7
lines changed

6 files changed

+403
-7
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ default: &default
2828
port: 8123
2929
username: username
3030
password: password
31+
http_auth: query_params # optional, supports query_params, basic, x_clickhouse_headers
3132
ssl: true # optional for using ssl connection
3233
debug: true # use for showing in to log technical information
3334
migrations_paths: db/clickhouse # optional, default: db/migrate_clickhouse
@@ -53,6 +54,28 @@ class ActionView < ActiveRecord::Base
5354
end
5455
```
5556

57+
### HTTP authentication mode
58+
59+
By default, the adapter sends `user` and `password` as URL parameters.
60+
You can set `http_auth` explicitly (or omit it and keep the same default behavior):
61+
62+
```yml
63+
clickhouse:
64+
adapter: clickhouse
65+
host: localhost
66+
port: 8123
67+
database: my_db
68+
username: app_user
69+
password: secret
70+
http_auth: x_clickhouse_headers # or basic / query_params
71+
```
72+
73+
- Use YAML string values: `http_auth: query_params`, `http_auth: basic`, or `http_auth: x_clickhouse_headers`.
74+
- Both strings and Ruby symbols are accepted internally.
75+
- `http_auth: x_clickhouse_headers` sends `X-ClickHouse-User`, `X-ClickHouse-Key`, and `X-ClickHouse-Database` headers.
76+
- `http_auth: basic` sends `Authorization: Basic ...` and keeps `database` in URL params.
77+
- `http_auth: query_params` sends `user`, `password`, and `database` in URL params (same as omitting `http_auth`).
78+
5679
## Usage in Rails
5780

5881
Add your `database.yml` connection information with postfix `_clickhouse` for you environment:
@@ -343,6 +366,38 @@ Donations to this project are going directly to [PNixx](https://github.com/PNixx
343366

344367
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
345368

369+
### Run locally
370+
371+
1. Start ClickHouse (single node):
372+
373+
```bash
374+
docker compose -f .docker/docker-compose.yml up -d
375+
```
376+
377+
2. Run single-node specs:
378+
379+
```bash
380+
bin/test-single
381+
```
382+
383+
If your local workflow expects `bin/single_test`, use the same command format as `bin/test-single`:
384+
385+
```bash
386+
CLICKHOUSE_PORT=18123 CLICKHOUSE_DATABASE=default bundle exec rspec spec/single --format progress
387+
```
388+
389+
3. Start ClickHouse cluster:
390+
391+
```bash
392+
docker compose -f .docker/docker-compose.cluster.yml up -d
393+
```
394+
395+
4. Run cluster specs:
396+
397+
```bash
398+
CLICKHOUSE_PORT=28123 CLICKHOUSE_DATABASE=default CLICKHOUSE_CLUSTER=test_cluster bundle exec rspec spec/cluster --format progress
399+
```
400+
346401
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
347402

348403
Testing github actions:

bin/console

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env ruby
22

33
require "bundler/setup"
4-
require "clickhouse/activerecord"
4+
require "clickhouse-activerecord"
55

66
# You can add fixtures and/or initialization code here to make experimenting
77
# with your gem easier. You can also use a different console, if you like.

bin/test-single

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
export CLICKHOUSE_PORT="${CLICKHOUSE_PORT:-18123}"
5+
export CLICKHOUSE_DATABASE="${CLICKHOUSE_DATABASE:-default}"
6+
7+
exec bundle exec rspec spec/single --format progress "$@"

lib/active_record/connection_adapters/clickhouse/schema_statements.rb

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
# frozen_string_literal: true
22

3+
require 'base64'
34
require 'clickhouse-activerecord/version'
45

56
module ActiveRecord
67
module ConnectionAdapters
78
module Clickhouse
89
module SchemaStatements
10+
HTTP_AUTH_QUERY_PARAMS = :query_params
11+
HTTP_AUTH_BASIC = :basic
12+
HTTP_AUTH_X_HEADERS = :x_clickhouse_headers
13+
HTTP_AUTH_TYPES = [HTTP_AUTH_QUERY_PARAMS, HTTP_AUTH_BASIC, HTTP_AUTH_X_HEADERS].freeze
914

1015
def with_settings(**settings)
1116
@block_settings ||= {}
@@ -61,10 +66,7 @@ def execute_to_file(sql, name = nil, format: @response_format, settings: {})
6166
statement = Statement.new(sql, format: @response_format)
6267
result = nil
6368
@lock.synchronize do
64-
req = Net::HTTP::Post.new("/?#{settings_params(settings)}", {
65-
'Content-Type' => 'application/x-www-form-urlencoded',
66-
'User-Agent' => ClickhouseAdapter::USER_AGENT,
67-
})
69+
req = Net::HTTP::Post.new("/?#{settings_params(settings)}", build_request_headers)
6870
@connection.start unless @connection.started?
6971
@connection.request(req, statement.formatted_sql) do |response|
7072
result = statement.streaming_response(response)
@@ -303,8 +305,7 @@ def request(statement, settings: {}, except_params: [])
303305
@lock.synchronize do
304306
@connection.post("/?#{settings_params(settings, except: except_params)}",
305307
statement.formatted_sql,
306-
'Content-Type' => 'application/x-www-form-urlencoded',
307-
'User-Agent' => ClickhouseAdapter::USER_AGENT)
308+
build_request_headers(include_database: !except_params.include?(:database)))
308309
end
309310
end
310311

@@ -316,12 +317,41 @@ def log_with_debug(sql, name = nil)
316317
def settings_params(settings = {}, except: [])
317318
request_params = @connection_config || {}
318319
block_settings = @block_settings || {}
320+
321+
case @http_auth
322+
when HTTP_AUTH_BASIC
323+
request_params = request_params.except(:user, :password)
324+
when HTTP_AUTH_X_HEADERS
325+
request_params = request_params.except(:user, :password, :database)
326+
end
327+
319328
request_params.merge(block_settings)
320329
.merge(settings)
321330
.except(*except)
322331
.to_param
323332
end
324333

334+
def build_request_headers(include_database: true)
335+
request_headers = {
336+
'Content-Type' => 'application/x-www-form-urlencoded',
337+
'User-Agent' => ClickhouseAdapter::USER_AGENT
338+
}
339+
340+
case @http_auth
341+
when HTTP_AUTH_BASIC
342+
if @config[:username] && @config[:password]
343+
credentials = Base64.strict_encode64("#{@config[:username]}:#{@config[:password]}")
344+
request_headers['Authorization'] = "Basic #{credentials}"
345+
end
346+
when HTTP_AUTH_X_HEADERS
347+
request_headers['X-ClickHouse-User'] = @config[:username].to_s if @config[:username]
348+
request_headers['X-ClickHouse-Key'] = @config[:password].to_s if @config[:password]
349+
request_headers['X-ClickHouse-Database'] = @config[:database].to_s if include_database && @config[:database]
350+
end
351+
352+
request_headers
353+
end
354+
325355
# Returns a hash of table names to their engine types
326356
def table_engines(table_names = nil)
327357
table_names_sql = if table_names.present?

lib/active_record/connection_adapters/clickhouse_adapter.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ def clickhouse_connection(config)
3232
raise ArgumentError, 'No database specified. Missing argument: database.'
3333
end
3434

35+
if config[:http_auth]
36+
unless ConnectionAdapters::Clickhouse::SchemaStatements::HTTP_AUTH_TYPES.include?(config[:http_auth]&.to_sym)
37+
raise ArgumentError, "Unknown :http_auth mode #{config[:http_auth].inspect}. " \
38+
+ "Use one of #{ConnectionAdapters::Clickhouse::SchemaStatements::HTTP_AUTH_TYPES}."
39+
end
40+
end
41+
3542
ConnectionAdapters::ClickhouseAdapter.new(config)
3643
end
3744
end
@@ -143,6 +150,7 @@ def initialize(config_or_deprecated_connection, deprecated_logger = nil, depreca
143150
@connection_config = { user: @config[:username], password: @config[:password], database: @config[:database] }.compact
144151
@debug = @config[:debug] || false
145152
@response_format = @config[:format] || DEFAULT_RESPONSE_FORMAT
153+
@http_auth = @config[:http_auth]&.to_sym
146154

147155
@prepared_statements = false
148156

0 commit comments

Comments
 (0)