Skip to content

Commit 2fc8fbc

Browse files
authored
Merge pull request kubernetes-client#333 from brendandburns/reload
Add a refreshing auth token for in-cluster configurations.
2 parents 0a0dcbf + 9bf556c commit 2fc8fbc

File tree

10 files changed

+230
-77
lines changed

10 files changed

+230
-77
lines changed

src/auth.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,5 @@ import { User } from './config_types';
55

66
export interface Authenticator {
77
isAuthProvider(user: User): boolean;
8-
// TODO: Deprecate this and roll it into applyAuthentication
9-
getToken(user: User): string | null;
108
applyAuthentication(user: User, opts: request.Options | https.RequestOptions): Promise<void>;
119
}

src/cloud_auth.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,19 @@ export class CloudAuth implements Authenticator {
2626
return user.authProvider.name === 'azure' || user.authProvider.name === 'gcp';
2727
}
2828

29-
public getToken(user: User): string | null {
29+
public async applyAuthentication(user: User, opts: request.Options | https.RequestOptions) {
30+
const token = this.getToken(user);
31+
if (token) {
32+
opts.headers!.Authorization = `Bearer ${token}`;
33+
}
34+
}
35+
36+
private getToken(user: User): string | null {
3037
const config = user.authProvider.config;
3138
if (this.isExpired(config)) {
3239
this.updateAccessToken(config);
3340
}
34-
return 'Bearer ' + config['access-token'];
35-
}
36-
37-
public async applyAuthentication(user: User, opts: request.Options | https.RequestOptions) {
38-
// pass
41+
return config['access-token'];
3942
}
4043

4144
private isExpired(config: Config) {

src/config.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Authenticator } from './auth';
1212
import { CloudAuth } from './cloud_auth';
1313
import { Cluster, Context, newClusters, newContexts, newUsers, User } from './config_types';
1414
import { ExecAuth } from './exec_auth';
15+
import { FileAuth } from './file_auth';
1516
import { OpenIDConnectAuth } from './oidc_auth';
1617

1718
// fs.existsSync was removed in node 10
@@ -28,6 +29,7 @@ export class KubeConfig {
2829
private static authenticators: Authenticator[] = [
2930
new CloudAuth(),
3031
new ExecAuth(),
32+
new FileAuth(),
3133
new OpenIDConnectAuth(),
3234
];
3335

@@ -190,7 +192,12 @@ export class KubeConfig {
190192
this.users = [
191193
{
192194
name: userName,
193-
token: fs.readFileSync(`${pathPrefix}${Config.SERVICEACCOUNT_TOKEN_PATH}`).toString(),
195+
authProvider: {
196+
name: 'tokenFile',
197+
config: {
198+
tokenFile: `${pathPrefix}${Config.SERVICEACCOUNT_TOKEN_PATH}`,
199+
},
200+
},
194201
},
195202
];
196203
this.contexts = [
@@ -350,21 +357,15 @@ export class KubeConfig {
350357
return elt.isAuthProvider(user);
351358
});
352359

353-
let token: string | null = null;
360+
if (!opts.headers) {
361+
opts.headers = [];
362+
}
354363
if (authenticator) {
355-
token = authenticator.getToken(user);
356364
await authenticator.applyAuthentication(user, opts);
357365
}
358366

359367
if (user.token) {
360-
token = 'Bearer ' + user.token;
361-
}
362-
363-
if (token) {
364-
if (!opts.headers) {
365-
opts.headers = [];
366-
}
367-
opts.headers.Authorization = token;
368+
opts.headers.Authorization = `Bearer ${user.token}`;
368369
}
369370
}
370371

src/config_test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ describe('KubeConfig', () => {
205205
kc.applytoHTTPSOptions(opts);
206206

207207
expect(opts).to.deep.equal({
208+
headers: [],
208209
ca: new Buffer('CADATA2', 'utf-8'),
209210
cert: new Buffer('USER2_CADATA', 'utf-8'),
210211
key: new Buffer('USER2_CKDATA', 'utf-8'),
@@ -221,6 +222,7 @@ describe('KubeConfig', () => {
221222
};
222223
await kc.applyToRequest(opts);
223224
expect(opts).to.deep.equal({
225+
headers: [],
224226
ca: new Buffer('CADATA2', 'utf-8'),
225227
auth: {
226228
username: 'foo',
@@ -1000,7 +1002,9 @@ describe('KubeConfig', () => {
10001002
const user = kc.getCurrentUser();
10011003
expect(user).to.not.be.null;
10021004
if (user) {
1003-
expect(user.token).to.equal(token);
1005+
expect(user.authProvider.config.tokenFile).to.equal(
1006+
'/var/run/secrets/kubernetes.io/serviceaccount/token',
1007+
);
10041008
}
10051009
});
10061010

src/exec_auth.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,6 @@ export class ExecAuth implements Authenticator {
3636
);
3737
}
3838

39-
public getToken(user: User): string | null {
40-
const credential = this.getCredential(user);
41-
if (!credential) {
42-
return null;
43-
}
44-
if (credential.status.token) {
45-
return `Bearer ${credential.status.token}`;
46-
}
47-
return null;
48-
}
49-
5039
public async applyAuthentication(user: User, opts: request.Options | https.RequestOptions) {
5140
const credential = this.getCredential(user);
5241
if (!credential) {
@@ -58,6 +47,20 @@ export class ExecAuth implements Authenticator {
5847
if (credential.status.clientKeyData) {
5948
opts.key = credential.status.clientKeyData;
6049
}
50+
const token = this.getToken(credential);
51+
if (token) {
52+
opts.headers!.Authorization = `Bearer ${token}`;
53+
}
54+
}
55+
56+
private getToken(credential: Credential): string | null {
57+
if (!credential) {
58+
return null;
59+
}
60+
if (credential.status.token) {
61+
return credential.status.token;
62+
}
63+
return null;
6164
}
6265

6366
private getCredential(user: User): Credential | null {

src/exec_auth_test.ts

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { expect } from 'chai';
1+
import { expect, use } from 'chai';
2+
import chaiAsPromised = require('chai-as-promised');
3+
use(chaiAsPromised);
4+
25
import * as shell from 'shelljs';
36

47
import execa = require('execa');
@@ -61,18 +64,22 @@ describe('ExecAuth', () => {
6164
stdout: JSON.stringify({ status: { token: 'foo' } }),
6265
} as execa.ExecaSyncReturnValue;
6366
};
64-
65-
const token = auth.getToken({
66-
name: 'user',
67-
authProvider: {
68-
config: {
69-
exec: {
70-
command: 'echo',
67+
const opts = {} as request.Options;
68+
opts.headers = [];
69+
auth.applyAuthentication(
70+
{
71+
name: 'user',
72+
authProvider: {
73+
config: {
74+
exec: {
75+
command: 'echo',
76+
},
7177
},
7278
},
7379
},
74-
});
75-
expect(token).to.equal('Bearer foo');
80+
opts,
81+
);
82+
expect(opts.headers.Authorization).to.equal('Bearer foo');
7683
});
7784

7885
it('should correctly exec for certs', async () => {
@@ -98,11 +105,11 @@ describe('ExecAuth', () => {
98105
},
99106
},
100107
};
101-
const token = auth.getToken(user);
102-
expect(token).to.be.null;
103-
104108
const opts = {} as request.Options;
109+
opts.headers = [];
110+
105111
auth.applyAuthentication(user, opts);
112+
expect(opts.headers.Authorization).to.be.undefined;
106113
expect(opts.cert).to.equal('foo');
107114
expect(opts.key).to.equal('bar');
108115
});
@@ -136,28 +143,35 @@ describe('ExecAuth', () => {
136143
},
137144
},
138145
};
139-
var token = auth.getToken(user);
140-
expect(token).to.equal(`Bearer ${tokenValue}`);
146+
147+
const opts = {} as request.Options;
148+
opts.headers = [];
149+
150+
await auth.applyAuthentication(user, opts);
151+
expect(opts.headers.Authorization).to.equal(`Bearer ${tokenValue}`);
141152
expect(execCount).to.equal(1);
142153

143154
// old token should be expired, set expiration for the new token for the future.
144155
expire = '29 Mar 2095 00:00:00 GMT';
145156
tokenValue = 'bar';
146-
token = auth.getToken(user);
147-
expect(token).to.equal(`Bearer ${tokenValue}`);
157+
await auth.applyAuthentication(user, opts);
158+
expect(opts.headers.Authorization).to.equal(`Bearer ${tokenValue}`);
148159
expect(execCount).to.equal(2);
149160

150161
// Should use cached token, execCount should stay at two, token shouldn't change
151162
tokenValue = 'baz';
152-
token = auth.getToken(user);
153-
expect(token).to.equal('Bearer bar');
163+
await auth.applyAuthentication(user, opts);
164+
expect(opts.headers.Authorization).to.equal('Bearer bar');
154165
expect(execCount).to.equal(2);
155166
});
156167

157-
it('should return null on no exec info', () => {
168+
it('should return null on no exec info', async () => {
158169
const auth = new ExecAuth();
159-
const token = auth.getToken({} as User);
160-
expect(token).to.be.null;
170+
const opts = {} as request.Options;
171+
opts.headers = [];
172+
173+
await auth.applyAuthentication({} as User, opts);
174+
expect(opts.headers.Authorization).to.be.undefined;
161175
});
162176

163177
it('should throw on exec errors', () => {
@@ -184,8 +198,11 @@ describe('ExecAuth', () => {
184198
},
185199
},
186200
};
201+
const opts = {} as request.Options;
202+
opts.headers = [];
187203

188-
expect(() => auth.getToken(user)).to.throw('Some error!');
204+
const promise = auth.applyAuthentication(user, opts);
205+
return expect(promise).to.eventually.be.rejected;
189206
});
190207

191208
it('should exec with env vars', async () => {
@@ -203,22 +220,28 @@ describe('ExecAuth', () => {
203220
} as execa.ExecaSyncReturnValue;
204221
};
205222
process.env.BLABBLE = 'flubble';
206-
const token = auth.getToken({
207-
name: 'user',
208-
authProvider: {
209-
config: {
210-
exec: {
211-
command: 'echo',
212-
env: [
213-
{
214-
name: 'foo',
215-
value: 'bar',
216-
},
217-
],
223+
const opts = {} as request.Options;
224+
opts.headers = [];
225+
226+
await auth.applyAuthentication(
227+
{
228+
name: 'user',
229+
authProvider: {
230+
config: {
231+
exec: {
232+
command: 'echo',
233+
env: [
234+
{
235+
name: 'foo',
236+
value: 'bar',
237+
},
238+
],
239+
},
218240
},
219241
},
220242
},
221-
});
243+
opts,
244+
);
222245
expect(optsOut.env.foo).to.equal('bar');
223246
expect(optsOut.env.PATH).to.equal(process.env.PATH);
224247
expect(optsOut.env.BLABBLE).to.equal(process.env.BLABBLE);

src/file_auth.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import fs = require('fs');
2+
import https = require('https');
3+
import request = require('request');
4+
5+
import { Authenticator } from './auth';
6+
import { User } from './config_types';
7+
8+
export class FileAuth implements Authenticator {
9+
private token: string | null = null;
10+
private lastRead: Date | null = null;
11+
12+
public isAuthProvider(user: User): boolean {
13+
return user.authProvider && user.authProvider.config && user.authProvider.config.tokenFile;
14+
}
15+
16+
public async applyAuthentication(user: User, opts: request.Options | https.RequestOptions) {
17+
if (this.token == null) {
18+
this.refreshToken(user.authProvider.config.tokenFile);
19+
}
20+
if (this.isTokenExpired()) {
21+
this.refreshToken(user.authProvider.config.tokenFile);
22+
}
23+
if (this.token) {
24+
opts.headers!.Authorization = `Bearer ${this.token}`;
25+
}
26+
}
27+
28+
private refreshToken(filePath: string) {
29+
// TODO make this async?
30+
this.token = fs.readFileSync(filePath).toString('UTF-8');
31+
this.lastRead = new Date();
32+
}
33+
34+
private isTokenExpired(): boolean {
35+
if (this.lastRead === null) {
36+
return true;
37+
}
38+
const now = new Date();
39+
const delta = (now.getTime() - this.lastRead.getTime()) / 1000;
40+
// For now just refresh every 60 seconds. This is imperfect since the token
41+
// could be out of date for this time, but it is unlikely and it's also what
42+
// the client-go library does.
43+
// TODO: Use file notifications instead?
44+
return delta > 60;
45+
}
46+
}

0 commit comments

Comments
 (0)