diff --git a/README.md b/README.md index 2f3b7097..b8f54d64 100644 --- a/README.md +++ b/README.md @@ -430,6 +430,24 @@ You can read it as follows: puts config.context.namespace ``` +### Impersonation + +Impersonation is supported when loading a kubectl config and via the Ruby API, for example: + +```ruby +client = Kubeclient::Client.new( + context.api_endpoint, 'v1', + auth_options: { + as: "admin", + as_groups: ["system:masters"], + as_uid: "123", # optional + as_user_extra: { + "reason" => ["admin access"] + } + } +) +``` + ### Supported kubernetes versions We try to support the last 3 minor versions, matching the [official support policy for Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/release/versioning.md#supported-releases-and-component-skew). diff --git a/lib/kubeclient.rb b/lib/kubeclient.rb index c82420ea..acfef1ec 100644 --- a/lib/kubeclient.rb +++ b/lib/kubeclient.rb @@ -128,6 +128,8 @@ def initialize_client( validate_bearer_token_file bearer_token(File.read(@auth_options[:bearer_token_file])) end + + configure_impersonation_headers end def configure_faraday(&block) @@ -675,7 +677,6 @@ def fetch_entities end def bearer_token(bearer_token) - @headers ||= {} @headers[:Authorization] = "Bearer #{bearer_token}" end @@ -702,6 +703,21 @@ def validate_bearer_token_file raise ArgumentError, msg unless File.readable?(@auth_options[:bearer_token_file]) end + # following https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation + def configure_impersonation_headers + return unless (auth_as = @auth_options[:as]) + @headers[:'Impersonate-User'] = auth_as + if (auth_as_groups = @auth_options[:as_groups]) + @headers[:'Impersonate-Group'] = Array(auth_as_groups).join + end + if (auth_as_uid = @auth_options[:as_uid]) + @headers[:'Impersonate-Uid'] = auth_as_uid + end + @auth_options[:as_user_extra]&.each do |k, v| + @headers[:"Impersonate-Extra-#{k}"] = Array(v).join + end + end + def return_or_yield_to_watcher(watcher, &block) return watcher unless block_given? diff --git a/lib/kubeclient/config.rb b/lib/kubeclient/config.rb index 9a5e030d..b615fbbd 100644 --- a/lib/kubeclient/config.rb +++ b/lib/kubeclient/config.rb @@ -180,6 +180,16 @@ def fetch_user_auth_options(user) options[attr.to_sym] = user[attr] if user.key?(attr) end end + + # TODO: allow setting Impersonate-Uid from here or comment on why it is not possible + [ + ['as', :as], + ['as-groups', :as_groups], + ['as-user-extra', :as_user_extra] + ].each do |k, v| + options[v] = user[k] if user.key?(k) + end + options end diff --git a/test/config/impersonate.kubeconfig b/test/config/impersonate.kubeconfig new file mode 100644 index 00000000..8a94aff1 --- /dev/null +++ b/test/config/impersonate.kubeconfig @@ -0,0 +1,22 @@ +apiVersion: v1 +clusters: +- cluster: + server: https://localhost:8443 + insecure-skip-tls-verify: true + name: localhost:8443 +contexts: +- context: + cluster: localhost:8443 + namespace: default + user: impersonate + name: localhost/impersonate +current-context: localhost/impersonate +kind: Config +preferences: {} +users: +- name: impersonate + user: + as: foo + as-groups: [bar, baz] + as-user-extra: + reason: [foo] diff --git a/test/test_config.rb b/test/test_config.rb index c3a2f94e..f0b7dadd 100644 --- a/test/test_config.rb +++ b/test/test_config.rb @@ -190,6 +190,19 @@ def test_oidc_auth_provider config.context(config.contexts.first) end + def test_impersonate + parsed = YAML.safe_load(File.read(config_file('impersonate.kubeconfig'))) + config = Kubeclient::Config.new(parsed, nil) + assert_equal( + { + as: 'foo', + as_groups: ['bar', 'baz'], + as_user_extra: { 'reason' => ['foo'] } + }, + config.context(config.contexts.first).auth_options + ) + end + private def check_context(context, ssl: true) diff --git a/test/test_kubeclient.rb b/test/test_kubeclient.rb index b8c6f32a..6a386630 100644 --- a/test/test_kubeclient.rb +++ b/test/test_kubeclient.rb @@ -791,6 +791,37 @@ def test_api_bearer_token_file_success assert_equal(1, pods.size) end + def test_impersonate + stub_request(:get, 'http://localhost:8080/api/v1/pods') + .with( + headers: { + Authorization: 'Bearer valid_token', + 'Impersonate-Extra-Reason': 'baz', + 'Impersonate-Group': 'bar', + 'Impersonate-User': 'foo', + 'Impersonate-Uid': '123' + } + ) + .to_return(body: { items: [] }.to_json) + stub_request(:get, %r{/api/v1$}) + .with(headers: { Authorization: 'Bearer valid_token' }) + .to_return(body: open_test_file('core_api_resource_list.json')) + + client = Kubeclient::Client.new( + 'http://localhost:8080/api/', + 'v1', + auth_options: { + bearer_token: 'valid_token', + as: 'foo', + as_groups: ['bar'], + as_user_extra: { 'reason' => ['baz'] }, + as_uid: '123' + } + ) + + client.get_pods + end + def test_proxy_url stub_core_api_list