Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion flipt-client-ruby/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
flipt_client (0.17.0)
flipt_client (1.0.0)

GEM
remote: https://rubygems.org/
Expand Down
103 changes: 103 additions & 0 deletions flipt-client-ruby/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ The `Flipt::Client` constructor accepts the following keyword arguments:
- `fetch_mode`: The fetch mode to use. Defaults to polling.
- `error_strategy`: The error strategy to use. Defaults to fail. See [Error Strategies](#error-strategies).
- `snapshot`: The snapshot to use when initializing the client. Defaults to no snapshot. See [Snapshotting](#snapshotting).
- `tls_config`: The TLS configuration for connecting to servers with custom certificates. See [TLS Configuration](#tls-configuration).

### Authentication

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

### TLS Configuration

The `Flipt::Client` supports configuring TLS settings for secure connections to Flipt servers. This is useful when:

- Connecting to Flipt servers with self-signed certificates
- Using custom Certificate Authorities (CAs)
- Implementing mutual TLS authentication
- Testing with insecure connections (development only)

#### Basic TLS with Custom CA Certificate

```ruby
# Using a CA certificate file
tls_config = Flipt::TlsConfig.with_ca_cert_file('/path/to/ca.pem')

client = Flipt::Client.new(
url: 'https://flipt.example.com',
tls_config: tls_config
)
```

```ruby
# Using CA certificate data directly
ca_cert_data = File.read('/path/to/ca.pem')
tls_config = Flipt::TlsConfig.with_ca_cert_data(ca_cert_data)

client = Flipt::Client.new(
url: 'https://flipt.example.com',
tls_config: tls_config
)
```

#### Mutual TLS Authentication

```ruby
# Using certificate and key files
tls_config = Flipt::TlsConfig.with_mutual_tls('/path/to/client.pem', '/path/to/client.key')

client = Flipt::Client.new(
url: 'https://flipt.example.com',
tls_config: tls_config
)
```

```ruby
# Using certificate and key data directly
client_cert_data = File.read('/path/to/client.pem')
client_key_data = File.read('/path/to/client.key')

tls_config = Flipt::TlsConfig.with_mutual_tls_data(client_cert_data, client_key_data)

client = Flipt::Client.new(
url: 'https://flipt.example.com',
tls_config: tls_config
)
```

#### Advanced TLS Configuration

```ruby
# Full TLS configuration with all options
tls_config = Flipt::TlsConfig.new(
ca_cert_file: '/path/to/ca.pem',
client_cert_file: '/path/to/client.pem',
client_key_file: '/path/to/client.key',
insecure_skip_verify: false
)

client = Flipt::Client.new(
url: 'https://flipt.example.com',
tls_config: tls_config
)
```

#### Development Mode (Insecure)

**⚠️ WARNING: Only use this in development environments!**

```ruby
# Skip certificate verification (NOT for production)
tls_config = Flipt::TlsConfig.insecure

client = Flipt::Client.new(
url: 'https://localhost:8443',
tls_config: tls_config
)
```

#### TLS Configuration Options

The `TlsConfig` class supports the following options:

- `ca_cert_file`: Path to custom CA certificate file (PEM format)
- `ca_cert_data`: Raw CA certificate content (PEM format) - takes precedence over `ca_cert_file`
- `insecure_skip_verify`: Skip certificate verification (development only)
- `client_cert_file`: Client certificate file for mutual TLS (PEM format)
- `client_key_file`: Client private key file for mutual TLS (PEM format)
- `client_cert_data`: Raw client certificate content (PEM format) - takes precedence over `client_cert_file`
- `client_key_data`: Raw client private key content (PEM format) - takes precedence over `client_key_file`

> **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.

### Error Strategies

The client supports the following error strategies:
Expand Down
9 changes: 9 additions & 0 deletions flipt-client-ruby/lib/flipt_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ def self.libfile
# Note: Streaming is currently only supported when using the SDK with Flipt Cloud or Flipt v2.
# @option opts [Symbol] :error_strategy error strategy to use for the client (:fail or :fallback).
# @option opts [String] :snapshot snapshot to use when initializing the client
# @option opts [TlsConfig] :tls_config TLS configuration for connecting to servers with custom certificates
def initialize(**opts)
@namespace = opts.fetch(:namespace, 'default')

opts[:authentication] = validate_authentication(opts.fetch(:authentication, NoAuthentication.new))
opts[:fetch_mode] = validate_fetch_mode(opts.fetch(:fetch_mode, :polling))
opts[:error_strategy] = validate_error_strategy(opts.fetch(:error_strategy, :fail))
opts[:tls_config] = validate_tls_config(opts.fetch(:tls_config, nil))

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

raise ValidationError, 'invalid error strategy'
end

def validate_tls_config(tls_config)
return nil if tls_config.nil?
return tls_config.to_h if tls_config.is_a?(TlsConfig)

raise ValidationError, 'invalid tls_config: must be TlsConfig instance'
end
end

# Deprecation shim for EvaluationClient
Expand Down
101 changes: 101 additions & 0 deletions flipt-client-ruby/lib/flipt_client/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,107 @@ def strategy
end
end

# TlsConfig provides configuration for TLS connections to Flipt servers
class TlsConfig
attr_reader :ca_cert_file, :ca_cert_data, :insecure_skip_verify,
:client_cert_file, :client_key_file, :client_cert_data, :client_key_data

# Initialize TLS configuration
#
# @param ca_cert_file [String, nil] Path to CA certificate file (PEM format)
# @param ca_cert_data [String, nil] Raw CA certificate content (PEM format)
# @param insecure_skip_verify [Boolean, nil] Skip certificate verification (development only)
# @param client_cert_file [String, nil] Path to client certificate file (PEM format)
# @param client_key_file [String, nil] Path to client key file (PEM format)
# @param client_cert_data [String, nil] Raw client certificate content (PEM format)
# @param client_key_data [String, nil] Raw client key content (PEM format)
def initialize(ca_cert_file: nil, ca_cert_data: nil, insecure_skip_verify: nil,
client_cert_file: nil, client_key_file: nil,
client_cert_data: nil, client_key_data: nil)
@ca_cert_file = ca_cert_file
@ca_cert_data = ca_cert_data
@insecure_skip_verify = insecure_skip_verify
@client_cert_file = client_cert_file
@client_key_file = client_key_file
@client_cert_data = client_cert_data
@client_key_data = client_key_data

validate_files!
end

# Create TLS config for insecure connections (development only)
# WARNING: Only use this in development environments
#
# @return [TlsConfig] TLS config with certificate verification disabled
def self.insecure
new(insecure_skip_verify: true)
end

# Create TLS config with CA certificate file
#
# @param ca_cert_file [String] Path to CA certificate file
# @return [TlsConfig] TLS config with custom CA certificate
def self.with_ca_cert_file(ca_cert_file)
new(ca_cert_file: ca_cert_file)
end

# Create TLS config with CA certificate data
#
# @param ca_cert_data [String] CA certificate content in PEM format
# @return [TlsConfig] TLS config with custom CA certificate
def self.with_ca_cert_data(ca_cert_data)
new(ca_cert_data: ca_cert_data)
end

# Create TLS config for mutual TLS with certificate files
#
# @param client_cert_file [String] Path to client certificate file
# @param client_key_file [String] Path to client key file
# @return [TlsConfig] TLS config with mutual TLS
def self.with_mutual_tls(client_cert_file, client_key_file)
new(client_cert_file: client_cert_file, client_key_file: client_key_file)
end

# Create TLS config for mutual TLS with certificate data
#
# @param client_cert_data [String] Client certificate content in PEM format
# @param client_key_data [String] Client key content in PEM format
# @return [TlsConfig] TLS config with mutual TLS
def self.with_mutual_tls_data(client_cert_data, client_key_data)
new(client_cert_data: client_cert_data, client_key_data: client_key_data)
end

# Convert to hash for JSON serialization
# @return [Hash] TLS configuration as hash
def to_h
hash = {}
hash[:ca_cert_file] = @ca_cert_file if @ca_cert_file
hash[:ca_cert_data] = @ca_cert_data if @ca_cert_data
hash[:insecure_skip_verify] = @insecure_skip_verify unless @insecure_skip_verify.nil?
hash[:client_cert_file] = @client_cert_file if @client_cert_file
hash[:client_key_file] = @client_key_file if @client_key_file
hash[:client_cert_data] = @client_cert_data if @client_cert_data
hash[:client_key_data] = @client_key_data if @client_key_data
hash
end

private

def validate_files!
validate_file_exists(@ca_cert_file, 'CA certificate file') if @ca_cert_file
validate_file_exists(@client_cert_file, 'Client certificate file') if @client_cert_file
validate_file_exists(@client_key_file, 'Client key file') if @client_key_file
end

def validate_file_exists(file_path, description)
return if file_path.nil? || file_path.strip.empty?

return if File.exist?(file_path)

raise ValidationError, "#{description} does not exist: #{file_path}"
end
end

# VariantEvaluationResponse
# @attr_reader [String] flag_key
# @attr_reader [Boolean] match
Expand Down
19 changes: 18 additions & 1 deletion flipt-client-ruby/spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,24 @@
before(:all) do
url = ENV.fetch('FLIPT_URL', 'http://localhost:8080')
auth_token = ENV.fetch('FLIPT_AUTH_TOKEN', 'secret')
@client = Flipt::Client.new(url: url, authentication: Flipt::ClientTokenAuthentication.new(auth_token))

# Configure TLS if HTTPS URL is provided
tls_config = nil
if url.start_with?('https://')
ca_cert_path = ENV['FLIPT_CA_CERT_PATH']
tls_config = if ca_cert_path && !ca_cert_path.empty?
Flipt::TlsConfig.with_ca_cert_file(ca_cert_path)
else
# Fallback to insecure for local testing
Flipt::TlsConfig.insecure
end
end

@client = Flipt::Client.new(
url: url,
authentication: Flipt::ClientTokenAuthentication.new(auth_token),
tls_config: tls_config
)
end

describe '#evaluate_variant' do
Expand Down
10 changes: 6 additions & 4 deletions test/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ var (
}

rubyVersions = []containerConfig{
{base: "ruby:3.1-bookworm"},
{base: "ruby:3.1-bullseye"},
{base: "ruby:3.1-alpine", setup: []string{"apk update", "apk add --no-cache build-base"}},
{base: "ruby:3.1-bookworm", useHTTPS: true},
{base: "ruby:3.1-bullseye", useHTTPS: true},
{base: "ruby:3.1-alpine", setup: []string{"apk update", "apk add --no-cache build-base"}, useHTTPS: true},
}

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