Skip to content

Commit 143e715

Browse files
authored
feat: add TLS configuration support to Ruby SDK (#1136)
* feat: add TLS configuration support to Java SDK Signed-off-by: Mark Phelps <[email protected]> * chore: fmt java Signed-off-by: Mark Phelps <[email protected]> * chore: update java tests to use https Signed-off-by: Mark Phelps <[email protected]> * chore: update java test params for https Signed-off-by: Mark Phelps <[email protected]> * chore: add debug info for failing tests Signed-off-by: Mark Phelps <[email protected]> * fix: TlsConfig serialization compatibility with Rust FFI Change TlsConfig from Lombok @value to traditional class structure to match ClientOptions pattern and ensure proper JSON serialization Signed-off-by: Mark Phelps <[email protected]> * chore: fmt Signed-off-by: Mark Phelps <[email protected]> * chore: rm debug env vars in java tests Signed-off-by: Mark Phelps <[email protected]> * chore: mount tls file Signed-off-by: Mark Phelps <[email protected]> * chore: add default for fetcher builder Signed-off-by: Mark Phelps <[email protected]> * chore: check for file existence in builder Signed-off-by: Mark Phelps <[email protected]> * chore: add file existence checks for python too Signed-off-by: Mark Phelps <[email protected]> * chore: fmt Signed-off-by: Mark Phelps <[email protected]> * chore: default to 120 seconds on java builder Signed-off-by: Mark Phelps <[email protected]> * chore: only set request_timeout and update_interval in fetcher if > 0 Signed-off-by: Mark Phelps <[email protected]> * feat: add TLS configuration support to Ruby SDK and enable HTTPS testing - Add TlsConfig class with comprehensive TLS options matching Python/Java implementations - Support CA certificates, client certificates for mutual TLS, and insecure mode - Add convenience factory methods for common TLS scenarios - Update integration test framework to use HTTPS with TLS certificates for Ruby tests - Add comprehensive README documentation with TLS configuration examples Signed-off-by: Mark Phelps <[email protected]> * chore: rubocop fmt Signed-off-by: Mark Phelps <[email protected]> --------- Signed-off-by: Mark Phelps <[email protected]>
1 parent e77a3d1 commit 143e715

File tree

6 files changed

+238
-6
lines changed

6 files changed

+238
-6
lines changed

flipt-client-ruby/Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
flipt_client (0.17.0)
4+
flipt_client (1.0.0)
55

66
GEM
77
remote: https://rubygems.org/

flipt-client-ruby/README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ The `Flipt::Client` constructor accepts the following keyword arguments:
114114
- `fetch_mode`: The fetch mode to use. Defaults to polling.
115115
- `error_strategy`: The error strategy to use. Defaults to fail. See [Error Strategies](#error-strategies).
116116
- `snapshot`: The snapshot to use when initializing the client. Defaults to no snapshot. See [Snapshotting](#snapshotting).
117+
- `tls_config`: The TLS configuration for connecting to servers with custom certificates. See [TLS Configuration](#tls-configuration).
117118

118119
### Authentication
119120

@@ -123,6 +124,108 @@ The `Flipt::Client` supports the following authentication strategies:
123124
- [Client Token Authentication](https://docs.flipt.io/authentication/using-tokens)
124125
- [JWT Authentication](https://docs.flipt.io/authentication/using-jwts)
125126

127+
### TLS Configuration
128+
129+
The `Flipt::Client` supports configuring TLS settings for secure connections to Flipt servers. This is useful when:
130+
131+
- Connecting to Flipt servers with self-signed certificates
132+
- Using custom Certificate Authorities (CAs)
133+
- Implementing mutual TLS authentication
134+
- Testing with insecure connections (development only)
135+
136+
#### Basic TLS with Custom CA Certificate
137+
138+
```ruby
139+
# Using a CA certificate file
140+
tls_config = Flipt::TlsConfig.with_ca_cert_file('/path/to/ca.pem')
141+
142+
client = Flipt::Client.new(
143+
url: 'https://flipt.example.com',
144+
tls_config: tls_config
145+
)
146+
```
147+
148+
```ruby
149+
# Using CA certificate data directly
150+
ca_cert_data = File.read('/path/to/ca.pem')
151+
tls_config = Flipt::TlsConfig.with_ca_cert_data(ca_cert_data)
152+
153+
client = Flipt::Client.new(
154+
url: 'https://flipt.example.com',
155+
tls_config: tls_config
156+
)
157+
```
158+
159+
#### Mutual TLS Authentication
160+
161+
```ruby
162+
# Using certificate and key files
163+
tls_config = Flipt::TlsConfig.with_mutual_tls('/path/to/client.pem', '/path/to/client.key')
164+
165+
client = Flipt::Client.new(
166+
url: 'https://flipt.example.com',
167+
tls_config: tls_config
168+
)
169+
```
170+
171+
```ruby
172+
# Using certificate and key data directly
173+
client_cert_data = File.read('/path/to/client.pem')
174+
client_key_data = File.read('/path/to/client.key')
175+
176+
tls_config = Flipt::TlsConfig.with_mutual_tls_data(client_cert_data, client_key_data)
177+
178+
client = Flipt::Client.new(
179+
url: 'https://flipt.example.com',
180+
tls_config: tls_config
181+
)
182+
```
183+
184+
#### Advanced TLS Configuration
185+
186+
```ruby
187+
# Full TLS configuration with all options
188+
tls_config = Flipt::TlsConfig.new(
189+
ca_cert_file: '/path/to/ca.pem',
190+
client_cert_file: '/path/to/client.pem',
191+
client_key_file: '/path/to/client.key',
192+
insecure_skip_verify: false
193+
)
194+
195+
client = Flipt::Client.new(
196+
url: 'https://flipt.example.com',
197+
tls_config: tls_config
198+
)
199+
```
200+
201+
#### Development Mode (Insecure)
202+
203+
**⚠️ WARNING: Only use this in development environments!**
204+
205+
```ruby
206+
# Skip certificate verification (NOT for production)
207+
tls_config = Flipt::TlsConfig.insecure
208+
209+
client = Flipt::Client.new(
210+
url: 'https://localhost:8443',
211+
tls_config: tls_config
212+
)
213+
```
214+
215+
#### TLS Configuration Options
216+
217+
The `TlsConfig` class supports the following options:
218+
219+
- `ca_cert_file`: Path to custom CA certificate file (PEM format)
220+
- `ca_cert_data`: Raw CA certificate content (PEM format) - takes precedence over `ca_cert_file`
221+
- `insecure_skip_verify`: Skip certificate verification (development only)
222+
- `client_cert_file`: Client certificate file for mutual TLS (PEM format)
223+
- `client_key_file`: Client private key file for mutual TLS (PEM format)
224+
- `client_cert_data`: Raw client certificate content (PEM format) - takes precedence over `client_cert_file`
225+
- `client_key_data`: Raw client private key content (PEM format) - takes precedence over `client_key_file`
226+
227+
> **Note**: When both file paths and data are provided, the data fields take precedence. For example, if both `ca_cert_file` and `ca_cert_data` are set, `ca_cert_data` will be used.
228+
126229
### Error Strategies
127230

128231
The client supports the following error strategies:

flipt-client-ruby/lib/flipt_client.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,14 @@ def self.libfile
6565
# Note: Streaming is currently only supported when using the SDK with Flipt Cloud or Flipt v2.
6666
# @option opts [Symbol] :error_strategy error strategy to use for the client (:fail or :fallback).
6767
# @option opts [String] :snapshot snapshot to use when initializing the client
68+
# @option opts [TlsConfig] :tls_config TLS configuration for connecting to servers with custom certificates
6869
def initialize(**opts)
6970
@namespace = opts.fetch(:namespace, 'default')
7071

7172
opts[:authentication] = validate_authentication(opts.fetch(:authentication, NoAuthentication.new))
7273
opts[:fetch_mode] = validate_fetch_mode(opts.fetch(:fetch_mode, :polling))
7374
opts[:error_strategy] = validate_error_strategy(opts.fetch(:error_strategy, :fail))
75+
opts[:tls_config] = validate_tls_config(opts.fetch(:tls_config, nil))
7476

7577
@engine = self.class.initialize_engine(opts.to_json)
7678
ObjectSpace.define_finalizer(self, self.class.finalize(@engine))
@@ -223,6 +225,13 @@ def validate_error_strategy(error_strategy)
223225

224226
raise ValidationError, 'invalid error strategy'
225227
end
228+
229+
def validate_tls_config(tls_config)
230+
return nil if tls_config.nil?
231+
return tls_config.to_h if tls_config.is_a?(TlsConfig)
232+
233+
raise ValidationError, 'invalid tls_config: must be TlsConfig instance'
234+
end
226235
end
227236

228237
# Deprecation shim for EvaluationClient

flipt-client-ruby/lib/flipt_client/models.rb

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,107 @@ def strategy
4141
end
4242
end
4343

44+
# TlsConfig provides configuration for TLS connections to Flipt servers
45+
class TlsConfig
46+
attr_reader :ca_cert_file, :ca_cert_data, :insecure_skip_verify,
47+
:client_cert_file, :client_key_file, :client_cert_data, :client_key_data
48+
49+
# Initialize TLS configuration
50+
#
51+
# @param ca_cert_file [String, nil] Path to CA certificate file (PEM format)
52+
# @param ca_cert_data [String, nil] Raw CA certificate content (PEM format)
53+
# @param insecure_skip_verify [Boolean, nil] Skip certificate verification (development only)
54+
# @param client_cert_file [String, nil] Path to client certificate file (PEM format)
55+
# @param client_key_file [String, nil] Path to client key file (PEM format)
56+
# @param client_cert_data [String, nil] Raw client certificate content (PEM format)
57+
# @param client_key_data [String, nil] Raw client key content (PEM format)
58+
def initialize(ca_cert_file: nil, ca_cert_data: nil, insecure_skip_verify: nil,
59+
client_cert_file: nil, client_key_file: nil,
60+
client_cert_data: nil, client_key_data: nil)
61+
@ca_cert_file = ca_cert_file
62+
@ca_cert_data = ca_cert_data
63+
@insecure_skip_verify = insecure_skip_verify
64+
@client_cert_file = client_cert_file
65+
@client_key_file = client_key_file
66+
@client_cert_data = client_cert_data
67+
@client_key_data = client_key_data
68+
69+
validate_files!
70+
end
71+
72+
# Create TLS config for insecure connections (development only)
73+
# WARNING: Only use this in development environments
74+
#
75+
# @return [TlsConfig] TLS config with certificate verification disabled
76+
def self.insecure
77+
new(insecure_skip_verify: true)
78+
end
79+
80+
# Create TLS config with CA certificate file
81+
#
82+
# @param ca_cert_file [String] Path to CA certificate file
83+
# @return [TlsConfig] TLS config with custom CA certificate
84+
def self.with_ca_cert_file(ca_cert_file)
85+
new(ca_cert_file: ca_cert_file)
86+
end
87+
88+
# Create TLS config with CA certificate data
89+
#
90+
# @param ca_cert_data [String] CA certificate content in PEM format
91+
# @return [TlsConfig] TLS config with custom CA certificate
92+
def self.with_ca_cert_data(ca_cert_data)
93+
new(ca_cert_data: ca_cert_data)
94+
end
95+
96+
# Create TLS config for mutual TLS with certificate files
97+
#
98+
# @param client_cert_file [String] Path to client certificate file
99+
# @param client_key_file [String] Path to client key file
100+
# @return [TlsConfig] TLS config with mutual TLS
101+
def self.with_mutual_tls(client_cert_file, client_key_file)
102+
new(client_cert_file: client_cert_file, client_key_file: client_key_file)
103+
end
104+
105+
# Create TLS config for mutual TLS with certificate data
106+
#
107+
# @param client_cert_data [String] Client certificate content in PEM format
108+
# @param client_key_data [String] Client key content in PEM format
109+
# @return [TlsConfig] TLS config with mutual TLS
110+
def self.with_mutual_tls_data(client_cert_data, client_key_data)
111+
new(client_cert_data: client_cert_data, client_key_data: client_key_data)
112+
end
113+
114+
# Convert to hash for JSON serialization
115+
# @return [Hash] TLS configuration as hash
116+
def to_h
117+
hash = {}
118+
hash[:ca_cert_file] = @ca_cert_file if @ca_cert_file
119+
hash[:ca_cert_data] = @ca_cert_data if @ca_cert_data
120+
hash[:insecure_skip_verify] = @insecure_skip_verify unless @insecure_skip_verify.nil?
121+
hash[:client_cert_file] = @client_cert_file if @client_cert_file
122+
hash[:client_key_file] = @client_key_file if @client_key_file
123+
hash[:client_cert_data] = @client_cert_data if @client_cert_data
124+
hash[:client_key_data] = @client_key_data if @client_key_data
125+
hash
126+
end
127+
128+
private
129+
130+
def validate_files!
131+
validate_file_exists(@ca_cert_file, 'CA certificate file') if @ca_cert_file
132+
validate_file_exists(@client_cert_file, 'Client certificate file') if @client_cert_file
133+
validate_file_exists(@client_key_file, 'Client key file') if @client_key_file
134+
end
135+
136+
def validate_file_exists(file_path, description)
137+
return if file_path.nil? || file_path.strip.empty?
138+
139+
return if File.exist?(file_path)
140+
141+
raise ValidationError, "#{description} does not exist: #{file_path}"
142+
end
143+
end
144+
44145
# VariantEvaluationResponse
45146
# @attr_reader [String] flag_key
46147
# @attr_reader [Boolean] match

flipt-client-ruby/spec/client_spec.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,24 @@
66
before(:all) do
77
url = ENV.fetch('FLIPT_URL', 'http://localhost:8080')
88
auth_token = ENV.fetch('FLIPT_AUTH_TOKEN', 'secret')
9-
@client = Flipt::Client.new(url: url, authentication: Flipt::ClientTokenAuthentication.new(auth_token))
9+
10+
# Configure TLS if HTTPS URL is provided
11+
tls_config = nil
12+
if url.start_with?('https://')
13+
ca_cert_path = ENV['FLIPT_CA_CERT_PATH']
14+
tls_config = if ca_cert_path && !ca_cert_path.empty?
15+
Flipt::TlsConfig.with_ca_cert_file(ca_cert_path)
16+
else
17+
# Fallback to insecure for local testing
18+
Flipt::TlsConfig.insecure
19+
end
20+
end
21+
22+
@client = Flipt::Client.new(
23+
url: url,
24+
authentication: Flipt::ClientTokenAuthentication.new(auth_token),
25+
tls_config: tls_config
26+
)
1027
end
1128

1229
describe '#evaluate_variant' do

test/main.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ var (
8181
}
8282

8383
rubyVersions = []containerConfig{
84-
{base: "ruby:3.1-bookworm"},
85-
{base: "ruby:3.1-bullseye"},
86-
{base: "ruby:3.1-alpine", setup: []string{"apk update", "apk add --no-cache build-base"}},
84+
{base: "ruby:3.1-bookworm", useHTTPS: true},
85+
{base: "ruby:3.1-bullseye", useHTTPS: true},
86+
{base: "ruby:3.1-alpine", setup: []string{"apk update", "apk add --no-cache build-base"}, useHTTPS: true},
8787
}
8888

8989
javaVersions = []containerConfig{
@@ -447,8 +447,10 @@ func rubyTests(ctx context.Context, root *dagger.Container, t *testCase) error {
447447
WithWorkdir("/src").
448448
WithDirectory("/src", t.hostDir.Directory("flipt-client-ruby")).
449449
WithDirectory("/src/lib/ext/linux_"+string(t.arch), t.engine.Directory(libDir)).
450+
WithDirectory("/src/test/fixtures/tls", t.hostDir.Directory("test/fixtures/tls")).
450451
WithServiceBinding("flipt", t.flipt.AsService()).
451-
WithEnvVariable("FLIPT_URL", "http://flipt:8080").
452+
WithEnvVariable("FLIPT_URL", "https://flipt:8443").
453+
WithEnvVariable("FLIPT_CA_CERT_PATH", "/src/test/fixtures/tls/ca.crt").
452454
WithEnvVariable("FLIPT_AUTH_TOKEN", "secret").
453455
WithExec(args("bundle install")).
454456
WithExec(args("bundle exec rspec")).

0 commit comments

Comments
 (0)