Skip to content

Commit 007bbb3

Browse files
author
Pablo Galache
committed
Support certificate based authentication when using an exec command in the kubeconfig. Add unit tests
1 parent 693d9a8 commit 007bbb3

File tree

4 files changed

+179
-33
lines changed

4 files changed

+179
-33
lines changed

util/src/main/java/io/kubernetes/client/util/KubeConfig.java

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public class KubeConfig {
4848
public static final String ENV_HOME = "HOME";
4949
public static final String KUBEDIR = ".kube";
5050
public static final String KUBECONFIG = "config";
51+
public static final String CRED_TOKEN_KEY = "token";
52+
public static final String CRED_CLIENT_CERTIFICATE_DATA_KEY = "clientCertificateData";
53+
public static final String CRED_CLIENT_KEY_DATA_KEY = "clientKeyData";
5154
private static Map<String, Authenticator> authenticators = new HashMap<>();
5255

5356
// Note to the reader: I considered creating a Config object
@@ -198,11 +201,13 @@ public String getPassword() {
198201
}
199202

200203
@SuppressWarnings("unchecked")
201-
public String getAccessToken() {
204+
public Map<String, String> getCredentials() {
202205
if (currentUser == null) {
203206
return null;
204207
}
205208

209+
Map<String, String> credentials = new HashMap<>();
210+
206211
Object authProvider = currentUser.get("auth-provider");
207212
if (authProvider != null) {
208213
Map<String, Object> authProviderMap = (Map<String, Object>) authProvider;
@@ -221,25 +226,28 @@ public String getAccessToken() {
221226
}
222227
}
223228
}
224-
return auth.getToken(authConfig);
229+
credentials.put(CRED_TOKEN_KEY, auth.getToken(authConfig));
230+
return credentials;
225231
} else {
226232
log.error("Unknown auth provider: " + name);
227233
}
228234
}
229235
}
230-
String tokenViaExecCredential =
231-
tokenViaExecCredential((Map<String, Object>) currentUser.get("exec"));
232-
if (tokenViaExecCredential != null) {
233-
return tokenViaExecCredential;
236+
Map<String, String> credentialsViaExecCredential =
237+
credentialsViaExecCredential((Map<String, Object>) currentUser.get("exec"));
238+
if (credentialsViaExecCredential != null) {
239+
return credentialsViaExecCredential;
234240
}
235241
if (currentUser.containsKey("token")) {
236-
return (String) currentUser.get("token");
242+
credentials.put(CRED_TOKEN_KEY, (String) currentUser.get("token"));
243+
return credentials;
237244
}
238245
if (currentUser.containsKey("tokenFile")) {
239246
String tokenFile = (String) currentUser.get("tokenFile");
240247
try {
241248
byte[] data = Files.readAllBytes(FileSystems.getDefault().getPath(tokenFile));
242-
return new String(data, StandardCharsets.UTF_8);
249+
credentials.put(CRED_TOKEN_KEY, new String(data, StandardCharsets.UTF_8));
250+
return credentials;
243251
} catch (IOException ex) {
244252
log.error("Failed to read token file", ex);
245253
}
@@ -248,17 +256,20 @@ public String getAccessToken() {
248256
}
249257

250258
/**
251-
* Attempt to create an access token by running a configured external program.
259+
* Attempt to create an access token or client certificate by running a configured external program.
252260
*
253261
* @see <a
254262
* href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins">
255263
* Authenticating » client-go credential plugins</a>
256264
*/
257265
@SuppressWarnings("unchecked")
258-
private String tokenViaExecCredential(Map<String, Object> execMap) {
266+
private Map<String, String> credentialsViaExecCredential(Map<String, Object> execMap) {
259267
if (execMap == null) {
260268
return null;
261269
}
270+
271+
Map<String, String> credentials = new HashMap<>();
272+
262273
String apiVersion = (String) execMap.get("apiVersion");
263274
if (!"client.authentication.k8s.io/v1beta1".equals(apiVersion)
264275
&& !"client.authentication.k8s.io/v1alpha1".equals(apiVersion)) {
@@ -281,13 +292,19 @@ private String tokenViaExecCredential(Map<String, Object> execMap) {
281292
JsonObject status = root.getAsJsonObject().get("status").getAsJsonObject();
282293
JsonElement token = status.get("token");
283294
if (token == null) {
284-
// TODO handle clientCertificateData/clientKeyData
285-
// (KubeconfigAuthentication is not yet set up for that to be dynamic)
286-
log.warn("No token produced by {}", command);
287-
return null;
295+
if (status.get("clientCertificateData") != null && status.get("clientKeyData") != null) {
296+
log.debug("Obtained a client certificate from {}", command);
297+
credentials.put(CRED_CLIENT_CERTIFICATE_DATA_KEY, status.get("clientCertificateData").getAsString());
298+
credentials.put(CRED_CLIENT_KEY_DATA_KEY, status.get("clientKeyData").getAsString());
299+
return credentials;
300+
} else {
301+
log.warn("No token or certificates produced by {}", command);
302+
return null;
303+
}
288304
}
289305
log.debug("Obtained a token from {}", command);
290-
return token.getAsString();
306+
credentials.put(CRED_TOKEN_KEY, token.getAsString());
307+
return credentials;
291308
// TODO cache tokens between calls, up to .status.expirationTimestamp
292309
// TODO a 401 is supposed to force a refresh,
293310
// but KubeconfigAuthentication hardcodes AccessTokenAuthentication which does not support

util/src/main/java/io/kubernetes/client/util/credentials/KubeconfigAuthentication.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import io.kubernetes.client.openapi.ApiClient;
1616
import io.kubernetes.client.util.KubeConfig;
1717
import java.io.IOException;
18+
import java.nio.charset.StandardCharsets;
19+
import java.util.Map;
1820
import org.apache.commons.lang3.StringUtils;
1921

2022
/**
@@ -53,11 +55,20 @@ public KubeconfigAuthentication(final KubeConfig config) throws IOException {
5355
return;
5456
}
5557

56-
// 3. honors bearer token
57-
String token = config.getAccessToken();
58-
if (StringUtils.isNotEmpty(token)) {
59-
delegateAuthentication = new AccessTokenAuthentication(token);
60-
return;
58+
// 3. honors bearer token or client certificate generated by exec command
59+
Map<String, String> credentials = config.getCredentials();
60+
if (credentials != null) {
61+
if (StringUtils.isNotEmpty(credentials.get(KubeConfig.CRED_TOKEN_KEY))) {
62+
delegateAuthentication = new AccessTokenAuthentication(credentials.get(KubeConfig.CRED_TOKEN_KEY));
63+
return;
64+
} else if (StringUtils.isNotEmpty(credentials.get(KubeConfig.CRED_CLIENT_CERTIFICATE_DATA_KEY))
65+
&& StringUtils.isNotEmpty(credentials.get(KubeConfig.CRED_CLIENT_KEY_DATA_KEY))) {
66+
delegateAuthentication =
67+
new ClientCertificateAuthentication(
68+
credentials.get(KubeConfig.CRED_CLIENT_CERTIFICATE_DATA_KEY).getBytes(StandardCharsets.UTF_8),
69+
credentials.get(KubeConfig.CRED_CLIENT_KEY_DATA_KEY).getBytes(StandardCharsets.UTF_8));
70+
return;
71+
}
6172
}
6273

6374
// 4. falling back to dummy authentication

util/src/test/java/io/kubernetes/client/util/KubeConfigTest.java

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public class KubeConfigTest {
6161
@Test
6262
public void testToken() {
6363
KubeConfig config = KubeConfig.loadKubeConfig(new StringReader(KUBECONFIG_TOKEN));
64-
assertEquals(config.getAccessToken(), "foobaz");
64+
assertEquals(config.getCredentials().get(KubeConfig.CRED_TOKEN_KEY), "foobaz");
6565
}
6666

6767
@Test
@@ -73,7 +73,7 @@ public void testTokenFile() throws IOException {
7373
String replace = KUBECONFIG_TOKEN.replace("foobaz", tokenFile.getCanonicalPath());
7474
replace = replace.replace("token:", "tokenFile:");
7575
KubeConfig config = KubeConfig.loadKubeConfig(new StringReader(replace));
76-
assertEquals(config.getAccessToken(), token);
76+
assertEquals(config.getCredentials().get(KubeConfig.CRED_TOKEN_KEY), token);
7777
}
7878

7979
public static String GCP_CONFIG =
@@ -108,7 +108,7 @@ public void testGCPAuthProvider() {
108108
writer.close();
109109

110110
KubeConfig kc = KubeConfig.loadKubeConfig(new FileReader(config));
111-
assertEquals("fake-token", kc.getAccessToken());
111+
assertEquals("fake-token", kc.getCredentials().get(KubeConfig.CRED_TOKEN_KEY));
112112
} catch (Exception ex) {
113113
ex.printStackTrace();
114114
fail("Unexpected exception: " + ex);
@@ -147,7 +147,7 @@ public void testGCPAuthProviderStringDate() {
147147
writer.close();
148148

149149
KubeConfig kc = KubeConfig.loadKubeConfig(new FileReader(config));
150-
assertEquals("fake-token", kc.getAccessToken());
150+
assertEquals("fake-token", kc.getCredentials().get(KubeConfig.CRED_TOKEN_KEY));
151151
} catch (Exception ex) {
152152
ex.printStackTrace();
153153
fail("Unexpected exception: " + ex);
@@ -198,7 +198,7 @@ public void testGCPAuthProviderExpiredTokenWithinGCloud() {
198198
KubeConfig.registerAuthenticator(new GCPAuthenticator(mockPB, null));
199199
try {
200200
KubeConfig kc = KubeConfig.loadKubeConfig(new StringReader(gcpConfigExpiredToken));
201-
assertEquals("new-fake-token", kc.getAccessToken());
201+
assertEquals("new-fake-token", kc.getCredentials().get(KubeConfig.CRED_TOKEN_KEY));
202202
} catch (Exception ex) {
203203
ex.printStackTrace();
204204
fail("Unexpected exception: " + ex);
@@ -233,7 +233,7 @@ public void testGCPAuthProviderExpiredTokenWithoutGCloud() {
233233
KubeConfig.registerAuthenticator(new GCPAuthenticator(null, mockGC));
234234
try {
235235
KubeConfig kc = KubeConfig.loadKubeConfig(new StringReader(gcpConfigExpiredToken));
236-
assertEquals(fakeToken, kc.getAccessToken());
236+
assertEquals(fakeToken, kc.getCredentials().get(KubeConfig.CRED_TOKEN_KEY));
237237
} catch (Exception ex) {
238238
ex.printStackTrace();
239239
fail("Unexpected exception: " + ex);
@@ -262,7 +262,7 @@ public void testAzureAuthProvider() {
262262
+ " name: azure\n";
263263
try {
264264
KubeConfig kc = KubeConfig.loadKubeConfig(new StringReader(azureConfig));
265-
assertEquals("fake-azure-token", kc.getAccessToken());
265+
assertEquals("fake-azure-token", kc.getCredentials().get(KubeConfig.CRED_TOKEN_KEY));
266266
} catch (Exception ex) {
267267
ex.printStackTrace();
268268
fail("Unexpected exception: " + ex);
@@ -331,8 +331,8 @@ public void testRefreshToken() {
331331
fake.token = "someNewToken";
332332
fake.refresh = "refreshToken";
333333

334-
String token = config.getAccessToken();
335-
assertEquals(token, fake.token);
334+
Map<String, String> credentials = config.getCredentials();
335+
assertEquals(credentials.get(KubeConfig.CRED_TOKEN_KEY), fake.token);
336336
}
337337

338338
private static final String KUBECONFIG_EXEC =
@@ -360,7 +360,7 @@ public void testExecCredentials() throws Exception {
360360
}
361361
KubeConfig kc = KubeConfig.loadKubeConfig(new StringReader(KUBECONFIG_EXEC));
362362
kc.setFile(folder.newFile()); // just making sure it is ignored
363-
assertEquals("abc123", kc.getAccessToken());
363+
assertEquals("abc123", kc.getCredentials().get(KubeConfig.CRED_TOKEN_KEY));
364364
}
365365

366366
@Test
@@ -371,7 +371,7 @@ public void testExecCredentialsAlpha1() throws Exception {
371371
}
372372
KubeConfig kc =
373373
KubeConfig.loadKubeConfig(new StringReader(KUBECONFIG_EXEC.replace("v1beta1", "v1alpha1")));
374-
assertEquals("abc123", kc.getAccessToken());
374+
assertEquals("abc123", kc.getCredentials().get(KubeConfig.CRED_TOKEN_KEY));
375375
}
376376

377377
private static final String KUBECONFIG_EXEC_ENV =
@@ -403,7 +403,7 @@ public void testExecCredentialsEnv() throws Exception {
403403
}
404404

405405
KubeConfig kc = KubeConfig.loadKubeConfig(new StringReader(KUBECONFIG_EXEC_ENV));
406-
assertEquals("abc123", kc.getAccessToken());
406+
assertEquals("abc123", kc.getCredentials().get(KubeConfig.CRED_TOKEN_KEY));
407407
}
408408

409409
private static final String KUBECONFIG_EXEC_BASEDIR =
@@ -448,7 +448,36 @@ public void testExecCredentialsBasedir() throws Exception {
448448
try (FileReader reader = new FileReader(config)) {
449449
KubeConfig kc = KubeConfig.loadKubeConfig(reader);
450450
kc.setFile(config);
451-
assertEquals("abc123", kc.getAccessToken());
451+
assertEquals("abc123", kc.getCredentials().get(KubeConfig.CRED_TOKEN_KEY));
452452
}
453453
}
454+
455+
private static final String KUBECONFIG_EXEC_CERTIFICATE =
456+
"apiVersion: v1\n"
457+
+ "current-context: c\n"
458+
+ "contexts:\n"
459+
+ "- name: c\n"
460+
+ " context:\n"
461+
+ " user: u\n"
462+
+ "users:\n"
463+
+ "- name: u\n"
464+
+ " user:\n"
465+
+ " exec:\n"
466+
+ " apiVersion: client.authentication.k8s.io/v1beta1\n"
467+
+ " command: echo\n"
468+
+ " args:\n"
469+
+ " - >-\n"
470+
+ " {\"kind\":\"ExecCredential\",\"apiVersion\":\"client.authentication.k8s.io/v1beta1\", \"status\":{\"clientCertificateData\":\"cert\",\"clientKeyData\":\"key\"}}";
471+
472+
@Test
473+
public void testExecCredentialsCertificate() throws Exception {
474+
// TODO: test exec on Windows
475+
if (System.getProperty("os.name").contains("Windows")) {
476+
return;
477+
}
478+
KubeConfig kc = KubeConfig.loadKubeConfig(new StringReader(KUBECONFIG_EXEC_CERTIFICATE));
479+
assertEquals("cert", kc.getCredentials().get(KubeConfig.CRED_CLIENT_CERTIFICATE_DATA_KEY));
480+
assertEquals("key", kc.getCredentials().get(KubeConfig.CRED_CLIENT_KEY_DATA_KEY));
481+
assertNull(kc.getCredentials().get(KubeConfig.CRED_TOKEN_KEY));
482+
}
454483
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.util.credentials;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.mockito.ArgumentMatchers.any;
17+
import static org.mockito.Mockito.times;
18+
import static org.mockito.Mockito.verify;
19+
import static org.mockito.Mockito.when;
20+
21+
import io.kubernetes.client.util.KubeConfig;
22+
import java.io.IOException;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
import org.junit.Test;
26+
import org.junit.runner.RunWith;
27+
import org.mockito.Mock;
28+
import org.mockito.junit.MockitoJUnitRunner;
29+
30+
@RunWith(MockitoJUnitRunner.class)
31+
public class KubeconfigAuthenticationTest {
32+
33+
@Mock private KubeConfig kubeConfig;
34+
35+
@Test
36+
public void testCertificateAuthenticationFromExecCommand() throws IOException {
37+
Map<String, String> certCredentials = new HashMap<>();
38+
certCredentials.put(KubeConfig.CRED_CLIENT_CERTIFICATE_DATA_KEY, "cert");
39+
certCredentials.put(KubeConfig.CRED_CLIENT_KEY_DATA_KEY, "key");
40+
when(kubeConfig.getCredentials()).thenReturn(certCredentials);
41+
42+
KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig);
43+
44+
assertThat(kubeconfigAuthentication.getDelegateAuthentication())
45+
.isInstanceOf(ClientCertificateAuthentication.class);
46+
}
47+
48+
@Test
49+
public void testCertificateAuthenticationFromKubeConfig() throws IOException {
50+
when(kubeConfig.getDataOrFileRelative(any(), any())).thenReturn("data".getBytes());
51+
52+
KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig);
53+
54+
assertThat(kubeconfigAuthentication.getDelegateAuthentication())
55+
.isInstanceOf(ClientCertificateAuthentication.class);
56+
verify(kubeConfig, times(2)).getDataOrFileRelative(any(), any());
57+
}
58+
59+
@Test
60+
public void testUsernamePasswordAuthenticationFromKubeConfig() throws IOException {
61+
when(kubeConfig.getUsername()).thenReturn("user");
62+
when(kubeConfig.getPassword()).thenReturn("password");
63+
64+
KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig);
65+
66+
assertThat(kubeconfigAuthentication.getDelegateAuthentication())
67+
.isInstanceOf(UsernamePasswordAuthentication.class);
68+
}
69+
70+
@Test
71+
public void testAccessTokenAuthenticationFromExecComand() throws IOException {
72+
Map<String, String> certCredentials = new HashMap<>();
73+
certCredentials.put(KubeConfig.CRED_TOKEN_KEY, "token");
74+
when(kubeConfig.getCredentials()).thenReturn(certCredentials);
75+
76+
KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig);
77+
78+
assertThat(kubeconfigAuthentication.getDelegateAuthentication())
79+
.isInstanceOf(AccessTokenAuthentication.class);
80+
}
81+
82+
@Test
83+
public void testDummyAuthentication() throws IOException {
84+
KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig);
85+
86+
assertThat(kubeconfigAuthentication.getDelegateAuthentication())
87+
.isInstanceOf(DummyAuthentication.class);
88+
}
89+
}

0 commit comments

Comments
 (0)