Repository: kubernetes-client/javascript
Pull request #2373 (2025-04-15 15:27:00)

config: support username impersonation
This commit adds support for username impersonation. This does not implement group, UID, or extra impersonation.

Refs: https://github.com/kubernetes-client/javascript/issues/2355

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-client/javascript/pull/2373#" title="Author self-approved">cjihrig</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-client%2Fjavascript).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-client/javascript/blob/main/OWNERS)~~ [cjihrig]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
mstruebing: The changes looks good to me - should we wait for @Dimpison to confirm?
cjihrig: I'm OK with moving it forward since it's been a few days. I did test in a real cluster, and it seemed to work.
Dimpison: Apologies for the late reply. I will test it tomorrow morning and update here.
Dimpison: I might be doing smth wrong, or it seems like it works slightly differently than it's expected to work.
Below is the code that I used for testing 
```
import * as k8s from "@kubernetes/client-node";

const KUBECONFIG_PATH = "/Users/Dmytro_Kabachenko/.kube/config";
const KUBE_CONFIG = new k8s.KubeConfig();
KUBE_CONFIG.loadFromFile(KUBECONFIG_PATH);
const K8S_CLIENT = k8s.KubernetesObjectApi.makeApiClient(KUBE_CONFIG);
K8S_CLIENT.defaultNamespace = "my-namespace";

console.log(KUBE_CONFIG.getCurrentUser());

const secretTemplate = {
  apiVersion: "v1",
  kind: "Secret",
  metadata: {
    name: "my-secret",
  },
};

const secret = K8S_CLIENT.read(secretTemplate);

console.log(secret);
```

package.json
```
  "dependencies": {
    "@kubernetes/client-node": "file:../javascript",
```

git repo
```
❯ git remote show origin
* remote origin
  Fetch URL: https://github.com/cjihrig/javascript.git
  Push  URL: https://github.com/cjihrig/javascript.git
  HEAD branch: master
  Remote branches:
    doc              tracked
    impersonate      tracked
    master           tracked
    promises         tracked
    release-1.x      tracked
    revert           tracked
    test             tracked
    transpiled-tests tracked
    url-parse        tracked
  Local branches configured for 'git pull':
    impersonate merges with remote impersonate
    master      merges with remote master
  Local refs configured for 'git push':
    impersonate pushes to impersonate (up to date)
    master      pushes to master      (up to date)
❯ git status
On branch impersonate
Your branch is up to date with 'origin/impersonate'.

nothing to commit, working tree clean
```

kubeconfig user configuration
```
- name: my-user
  user:
    as: "my-namespace-sudo"
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - eks
      - get-token
      - --cluster-name=my-cluster
      command: aws
```

Output from the code execution
```
❯ node src/index.js
{
  authProvider: undefined,
  certData: undefined,
  certFile: undefined,
  exec: {
    apiVersion: 'client.authentication.k8s.io/v1beta1',
    args: [ 'eks', 'get-token', '--cluster-name=my-cluster' ],
    command: 'aws'
  },
  keyData: undefined,
  keyFile: undefined,
  name: 'my-user,
  token: undefined,
  password: undefined,
  username: undefined,
  impersonateUser: undefined
}
Promise { <pending> }
file:///Users/Dmytro_Kabachenko/javascript/dist/object.js:466
        throw new ApiException(response.httpStatusCode, 'Unsuccessful HTTP Request', await response.getBodyAsAny(), response.headers);
              ^

ApiException [Error]: HTTP-Code: 403
Message: Unsuccessful HTTP Request
Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"my-secret\\\" is forbidden: User \\\"AWS_ROLE_ARN\\\" cannot get resource \\\"secrets\\\" in API group \\\"\\\" in the namespace \\\"my-namespace\\\"\",\"reason\":\"Forbidden\",\"details\":{\"name\":\"my-secret\",\"kind\":\"secrets\"},\"code\":403}\n"
Headers: {SKIPPED}
    at KubernetesObjectApi.processResponse (file:///Users/Dmytro_Kabachenko/javascript/dist/object.js:466:15)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  code: 403,
  body: 'SKIPPED',
  headers: {SKIPPED}
}

Node.js v20.19.0
```
The kubectl works w/ no issues and returns the same 403 when I remove the `as` keyword in kubeconfig.

mstruebing: @Dimpison one obvious question: Did you rebuild the kubernetes client?

i.e. `npm install && npm run build`
brendandburns: I think that if you use `kubectl --v=10 ...` you should be able to see exactly what `kubectl` is sending across HTTP and then diff it with what we're sending.
cjihrig: Do you need to allow the `impersonate` verb?
cjihrig: > `impersonateUser: undefined`

I think this is the problem. When I add `as: impersonated-user` to my own kubeconfig and run `console.log(kc.getCurrentUser());` I see `impersonateUser: 'impersonated-user'` logged. This is how I tested that just now:

```js
import * as k8s from './dist/index.js';
const kc = new k8s.KubeConfig();
kc.loadFromDefault();
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
console.log(kc.getCurrentUser());
```

Patch:
diff --git a/src/config.ts b/src/config.ts
index 81bbd6f1..526ce208 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -582,6 +582,11 @@ export class KubeConfig implements SecurityAuthentication {
         if (key) {
             opts.key = key;
         }
+
+        if (user.impersonateUser != null) {
+            opts.headers ??= {};
+            opts.headers['Impersonate-User'] = user.impersonateUser;
+        }
     }
 
     private async applyAuthorizationHeader(
diff --git a/src/config_types.ts b/src/config_types.ts
index eed9e1b0..6b08084a 100644
--- a/src/config_types.ts
+++ b/src/config_types.ts
@@ -97,6 +97,7 @@ export interface User {
     readonly token?: string;
     readonly username?: string;
     readonly password?: string;
+    readonly impersonateUser?: string;
 }
 
 export function newUsers(a: any, opts?: Partial<ConfigOptions>): User[] {
@@ -113,6 +114,7 @@ export function exportUser(user: User): any {
     return {
         name: user.name,
         user: {
+            as: user.impersonateUser,
             'auth-provider': user.authProvider,
             'client-certificate-data': user.certData,
             'client-certificate': user.certFile,
@@ -143,6 +145,7 @@ function userIterator(onInvalidEntry: ActionOnInvalid): (elt: any, i: number, li
                 token: findToken(elt.user),
                 password: elt.user ? elt.user.password : null,
                 username: elt.user ? elt.user.username : null,
+                impersonateUser: elt.user ? elt.user.as : null,
             };
         } catch (err) {
             switch (onInvalidEntry) {


Test patch:
diff --git a/src/config_test.ts b/src/config_test.ts
index 400906d5..c2544a82 100644
--- a/src/config_test.ts
+++ b/src/config_test.ts
@@ -643,6 +643,24 @@ describe('KubeConfig', () => {
             strictEqual(username, users[0].username);
             strictEqual(name, users[0].name);
         });
+        it('should load impersonation information', () => {
+            const users = newUsers([
+                {
+                    name: 'some-name-1',
+                    user: {
+                        as: 'impersonated-user',
+                    },
+                },
+                {
+                    name: 'some-name-2',
+                    user: {},
+                },
+            ]);
+            strictEqual('some-name-1', users[0].name);
+            strictEqual('impersonated-user', users[0].impersonateUser);
+            strictEqual('some-name-2', users[1].name);
+            strictEqual(undefined, users[1].impersonateUser);
+        });
     });
 
     describe('findHome', () => {
@@ -1787,4 +1805,25 @@ describe('KubeConfig', () => {
             strictEqual(opts.headers!.Authorization, 'Bearer test-token');
         });
     });
+
+    describe('Impersonation', () => {
+        it('injects Impersonate-User header', async () => {
+            const kc = new KubeConfig();
+            const cluster: Cluster = {
+                name: 'test-cluster',
+                server: 'https://localhost:6443',
+                skipTLSVerify: false,
+            };
+            const user: User = {
+                name: 'test-user',
+                authProvider: 'custom',
+                impersonateUser: 'impersonate-user',
+            };
+
+            kc.loadFromClusterAndUser(cluster, user);
+            const opts: RequestOptions = {};
+            await kc.applyToHTTPSOptions(opts);
+            strictEqual(opts.headers!['Impersonate-User'], 'impersonate-user');
+        });
+    });
 });