Skip to content
This repository has been archived by the owner on Feb 24, 2024. It is now read-only.

Commit

Permalink
Merge pull request ManageIQ#453 from grnhse/exec-client-creds
Browse files Browse the repository at this point in the history
Add support for client-type ExecCredentials
  • Loading branch information
cben authored Aug 3, 2020
2 parents 795a9fb + c557d33 commit dcccc6a
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 27 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
Kubeclient release versioning follows [SemVer](https://semver.org/).

## 4.9.0 - 2020-08-03
### Added
- Support for `user: exec` credential plugins using TLS client auth (#453)

## 4.8.0 — 2020-07-03

### Added
Expand Down
14 changes: 11 additions & 3 deletions lib/kubeclient/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ def contexts
def context(context_name = nil)
cluster, user, namespace = fetch_context(context_name || @kcfg['current-context'])

if user.key?('exec')
exec_opts = expand_command_option(user['exec'], 'command')
user['exec_result'] = ExecCredentials.run(exec_opts)
end

ca_cert_data = fetch_cluster_ca_data(cluster)
client_cert_data = fetch_user_cert_data(user)
client_key_data = fetch_user_key_data(user)
Expand Down Expand Up @@ -134,6 +139,8 @@ def fetch_user_cert_data(user)
File.read(ext_file_path(user['client-certificate']))
elsif user.key?('client-certificate-data')
Base64.decode64(user['client-certificate-data'])
elsif user.key?('exec_result') && user['exec_result'].key?('clientCertificateData')
user['exec_result']['clientCertificateData']
end
end

Expand All @@ -142,16 +149,17 @@ def fetch_user_key_data(user)
File.read(ext_file_path(user['client-key']))
elsif user.key?('client-key-data')
Base64.decode64(user['client-key-data'])
elsif user.key?('exec_result') && user['exec_result'].key?('clientKeyData')
user['exec_result']['clientKeyData']
end
end

def fetch_user_auth_options(user)
options = {}
if user.key?('token')
options[:bearer_token] = user['token']
elsif user.key?('exec')
exec_opts = expand_command_option(user['exec'], 'command')
options[:bearer_token] = Kubeclient::ExecCredentials.token(exec_opts)
elsif user.key?('exec_result') && user['exec_result'].key?('token')
options[:bearer_token] = user['exec_result']['token']
elsif user.key?('auth-provider')
options[:bearer_token] = fetch_token_from_provider(user['auth-provider'])
else
Expand Down
37 changes: 33 additions & 4 deletions lib/kubeclient/exec_credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Kubeclient
# Inspired by https://github.com/kubernetes/client-go/blob/master/plugin/pkg/client/auth/exec/exec.go
class ExecCredentials
class << self
def token(opts)
def run(opts)
require 'open3'
require 'json'

Expand All @@ -25,7 +25,7 @@ def token(opts)

creds = JSON.parse(out)
validate_credentials(opts, creds)
creds['status']['token']
creds['status']
end

private
Expand All @@ -34,6 +34,36 @@ def validate_opts(opts)
raise KeyError, 'exec command is required' unless opts['command']
end

def validate_client_credentials_status(status)
has_client_cert_data = status.key?('clientCertificateData')
has_client_key_data = status.key?('clientKeyData')

if has_client_cert_data && !has_client_key_data
raise 'exec plugin didn\'t return client key data'
end

if !has_client_cert_data && has_client_key_data
raise 'exec plugin didn\'t return client certificate data'
end

has_client_cert_data && has_client_key_data
end

def validate_credentials_status(status)
raise 'exec plugin didn\'t return a status field' if status.nil?

has_client_credentials = validate_client_credentials_status(status)
has_token = status.key?('token')

if has_client_credentials && has_token
raise 'exec plugin returned both token and client data'
end

return if has_client_credentials || has_token

raise 'exec plugin didn\'t return a token or client data' unless has_token
end

def validate_credentials(opts, creds)
# out should have ExecCredential structure
raise 'invalid credentials' if creds.nil?
Expand All @@ -45,8 +75,7 @@ def validate_credentials(opts, creds)
"plugin returned version #{creds['apiVersion']}"
end

raise 'exec plugin didn\'t return a status field' if creds['status'].nil?
raise 'exec plugin didn\'t return a token' if creds['status']['token'].nil?
validate_credentials_status(creds['status'])
end

# Transform name/value pairs to hash
Expand Down
2 changes: 1 addition & 1 deletion lib/kubeclient/version.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Kubernetes REST-API Client
module Kubeclient
VERSION = '4.8.0'.freeze
VERSION = '4.9.0'.freeze
end
138 changes: 119 additions & 19 deletions test/test_exec_credentials.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
require_relative 'test_helper'
require 'open3'

# Unit tests for the ExecCredentials token provider
# Unit tests for the ExecCredentials provider
class ExecCredentialsTest < MiniTest::Test
def test_exec_opts_missing
expected_msg =
'exec options are required'
exception = assert_raises(ArgumentError) do
Kubeclient::ExecCredentials.token(nil)
Kubeclient::ExecCredentials.run(nil)
end
assert_equal(expected_msg, exception.message)
end
Expand All @@ -16,7 +16,7 @@ def test_exec_command_missing
expected_msg =
'exec command is required'
exception = assert_raises(KeyError) do
Kubeclient::ExecCredentials.token({})
Kubeclient::ExecCredentials.run({})
end
assert_equal(expected_msg, exception.message)
end
Expand All @@ -33,34 +33,134 @@ def test_exec_command_failure

Open3.stub(:capture3, [nil, err, st]) do
exception = assert_raises(RuntimeError) do
Kubeclient::ExecCredentials.token(opts)
Kubeclient::ExecCredentials.run(opts)
end
assert_equal(expected_msg, exception.message)
end
end

def test_token
def test_run_with_token_credentials
opts = { 'command' => 'dummy' }

credentials = {
'token' => '0123456789ABCDEF0123456789ABCDEF'
}

creds = JSON.dump(
'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
'status' => credentials
)

st = Minitest::Mock.new
st.expect(:success?, true)

Open3.stub(:capture3, [creds, nil, st]) do
assert_equal(credentials, Kubeclient::ExecCredentials.run(opts))
end
end

def test_run_with_client_credentials
opts = { 'command' => 'dummy' }

credentials = {
'clientCertificateData' => '0123456789ABCDEF0123456789ABCDEF',
'clientKeyData' => '0123456789ABCDEF0123456789ABCDEF'
}

creds = JSON.dump(
'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
'status' => credentials
)

st = Minitest::Mock.new
st.expect(:success?, true)

Open3.stub(:capture3, [creds, nil, st]) do
assert_equal(credentials, Kubeclient::ExecCredentials.run(opts))
end
end

def test_run_with_missing_client_certificate_data
opts = { 'command' => 'dummy' }

credentials = {
'clientKeyData' => '0123456789ABCDEF0123456789ABCDEF'
}

creds = JSON.dump(
'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
'status' => credentials
)

st = Minitest::Mock.new
st.expect(:success?, true)

expected_msg = 'exec plugin didn\'t return client certificate data'

Open3.stub(:capture3, [creds, nil, st]) do
exception = assert_raises(RuntimeError) do
Kubeclient::ExecCredentials.run(opts)
end
assert_equal(expected_msg, exception.message)
end
end

def test_run_with_missing_client_key_data
opts = { 'command' => 'dummy' }

credentials = {
'clientCertificateData' => '0123456789ABCDEF0123456789ABCDEF'
}

creds = JSON.dump(
'apiVersion': 'client.authentication.k8s.io/v1alpha1',
'status': {
'token': '0123456789ABCDEF0123456789ABCDEF'
}
'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
'status' => credentials
)

st = Minitest::Mock.new
st.expect(:success?, true)

expected_msg = 'exec plugin didn\'t return client key data'

Open3.stub(:capture3, [creds, nil, st]) do
assert_equal('0123456789ABCDEF0123456789ABCDEF', Kubeclient::ExecCredentials.token(opts))
exception = assert_raises(RuntimeError) do
Kubeclient::ExecCredentials.run(opts)
end
assert_equal(expected_msg, exception.message)
end
end

def test_run_with_client_data_and_token
opts = { 'command' => 'dummy' }

credentials = {
'clientCertificateData' => '0123456789ABCDEF0123456789ABCDEF',
'clientKeyData' => '0123456789ABCDEF0123456789ABCDEF',
'token' => '0123456789ABCDEF0123456789ABCDEF'
}

creds = JSON.dump(
'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
'status' => credentials
)

st = Minitest::Mock.new
st.expect(:success?, true)

expected_msg = 'exec plugin returned both token and client data'

Open3.stub(:capture3, [creds, nil, st]) do
exception = assert_raises(RuntimeError) do
Kubeclient::ExecCredentials.run(opts)
end
assert_equal(expected_msg, exception.message)
end
end

def test_status_missing
opts = { 'command' => 'dummy' }

creds = JSON.dump('apiVersion': 'client.authentication.k8s.io/v1alpha1')
creds = JSON.dump('apiVersion' => 'client.authentication.k8s.io/v1alpha1')

st = Minitest::Mock.new
st.expect(:success?, true)
Expand All @@ -69,28 +169,28 @@ def test_status_missing

Open3.stub(:capture3, [creds, nil, st]) do
exception = assert_raises(RuntimeError) do
Kubeclient::ExecCredentials.token(opts)
Kubeclient::ExecCredentials.run(opts)
end
assert_equal(expected_msg, exception.message)
end
end

def test_token_missing
def test_credentials_missing
opts = { 'command' => 'dummy' }

creds = JSON.dump(
'apiVersion': 'client.authentication.k8s.io/v1alpha1',
'status': {}
'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
'status' => {}
)

st = Minitest::Mock.new
st.expect(:success?, true)

expected_msg = 'exec plugin didn\'t return a token'
expected_msg = 'exec plugin didn\'t return a token or client data'

Open3.stub(:capture3, [creds, nil, st]) do
exception = assert_raises(RuntimeError) do
Kubeclient::ExecCredentials.token(opts)
Kubeclient::ExecCredentials.run(opts)
end
assert_equal(expected_msg, exception.message)
end
Expand All @@ -106,7 +206,7 @@ def test_api_version_mismatch
}

creds = JSON.dump(
'apiVersion': api_version
'apiVersion' => api_version
)

st = Minitest::Mock.new
Expand All @@ -117,7 +217,7 @@ def test_api_version_mismatch

Open3.stub(:capture3, [creds, nil, st]) do
exception = assert_raises(RuntimeError) do
Kubeclient::ExecCredentials.token(opts)
Kubeclient::ExecCredentials.run(opts)
end
assert_equal(expected_msg, exception.message)
end
Expand Down

0 comments on commit dcccc6a

Please sign in to comment.