Skip to content

Commit

Permalink
Add support for certificate based exec auth.
Browse files Browse the repository at this point in the history
  • Loading branch information
brendandburns committed Jul 24, 2019
1 parent 159b32d commit b676607
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 13 deletions.
5 changes: 5 additions & 0 deletions src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import https = require('https');
import request = require('request');

import { User } from './config_types';

export interface Authenticator {
isAuthProvider(user: User): boolean;
// TODO: Deprecate this and roll it into applyAuthentication
getToken(user: User): string | null;
applyAuthentication(user: User, opts: request.Options | https.RequestOptions);
}
6 changes: 6 additions & 0 deletions src/cloud_auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as proc from 'child_process';
import https = require('https');
import * as jsonpath from 'jsonpath-plus';
import request = require('request');

import { Authenticator } from './auth';
import { User } from './config_types';
Expand Down Expand Up @@ -32,6 +34,10 @@ export class CloudAuth implements Authenticator {
return 'Bearer ' + config['access-token'];
}

public applyAuthentication(user: User, opts: request.Options | https.RequestOptions) {
// pass
}

private isExpired(config: Config) {
const token = config['access-token'];
const expiry = config.expiry;
Expand Down
14 changes: 8 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,14 +346,16 @@ export class KubeConfig {
if (!user) {
return;
}
let token: string | null = null;

KubeConfig.authenticators.forEach((authenticator: Authenticator) => {
if (authenticator.isAuthProvider(user)) {
token = authenticator.getToken(user);
}
const authenticator = KubeConfig.authenticators.find((elt: Authenticator) => {
return elt.isAuthProvider(user);
});

let token: string | null = null;
if (authenticator) {
token = authenticator.getToken(user);
authenticator.applyAuthentication(user, opts);
}

if (user.token) {
token = 'Bearer ' + user.token;
}
Expand Down
48 changes: 41 additions & 7 deletions src/exec_auth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import execa = require('execa');
import https = require('https');
import request = require('request');

import { Authenticator } from './auth';
import { User } from './config_types';

export interface CredentialStatus {
readonly token: string;
readonly clientCertificateData: string;
readonly clientKeyData: string;
readonly expirationTimestamp: string;
}

export interface Credential {
readonly status: CredentialStatus;
}

export class ExecAuth implements Authenticator {
private readonly tokenCache: { [key: string]: any } = {};
private readonly tokenCache: { [key: string]: Credential | null } = {};
private execFn: (cmd: string, args: string[], opts: execa.SyncOptions) => execa.ExecaSyncReturnValue =
execa.sync;

Expand All @@ -24,15 +37,36 @@ export class ExecAuth implements Authenticator {
}

public getToken(user: User): string | null {
// TODO: Handle client cert auth here, requires auth refactor.
// See https://kubernetes.io/docs/reference/access-authn-authz/authentication/#input-and-output-formats
// for details on this protocol.
const credential = this.getCredential(user);
if (!credential) {
return null;
}
if (credential.status.token) {
return `Bearer ${credential.status.token}`;
}
return null;
}

public async applyAuthentication(user: User, opts: request.Options | https.RequestOptions) {
const credential = this.getCredential(user);
if (!credential) {
return;
}
if (credential.status.clientCertificateData) {
opts.cert = credential.status.clientCertificateData;
}
if (credential.status.clientKeyData) {
opts.key = credential.status.clientKeyData;
}
}

private getCredential(user: User): Credential | null {
// TODO: Add a unit test for token caching.
const cachedToken = this.tokenCache[user.name];
if (cachedToken) {
const date = Date.parse(cachedToken.status.expirationTimestamp);
if (date > Date.now()) {
return `Bearer ${cachedToken.status.token}`;
return cachedToken;
}
this.tokenCache[user.name] = null;
}
Expand All @@ -57,9 +91,9 @@ export class ExecAuth implements Authenticator {
}
const result = this.execFn(exec.command, exec.args, opts);
if (result.code === 0) {
const obj = JSON.parse(result.stdout);
const obj = JSON.parse(result.stdout) as Credential;
this.tokenCache[user.name] = obj;
return `Bearer ${obj.status.token}`;
return obj;
}
throw new Error(result.stderr);
}
Expand Down
34 changes: 34 additions & 0 deletions src/exec_auth_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { expect } from 'chai';
import * as shell from 'shelljs';

import execa = require('execa');
import request = require('request');

import { ExecAuth } from './exec_auth';
import { User } from './config_types';

Expand Down Expand Up @@ -73,6 +75,38 @@ describe('ExecAuth', () => {
expect(token).to.equal('Bearer foo');
});

it('should correctly exec for certs', async () => {
const auth = new ExecAuth();
(auth as any).execFn = (
command: string,
args: string[],
opts: execa.SyncOptions,
): execa.ExecaSyncReturnValue => {
return {
code: 0,
stdout: JSON.stringify({ status: { clientCertificateData: 'foo', clientKeyData: 'bar' } }),
} as execa.ExecaSyncReturnValue;
};

const user = {
name: 'user',
authProvider: {
config: {
exec: {
command: 'echo',
},
},
},
};
const token = auth.getToken(user);
expect(token).to.be.null;

const opts = {} as request.Options;
auth.applyAuthentication(user, opts);
expect(opts.cert).to.equal('foo');
expect(opts.key).to.equal('bar');
});

it('should correctly exec and cache', async () => {
const auth = new ExecAuth();
var execCount = 0;
Expand Down
7 changes: 7 additions & 0 deletions src/oidc_auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import https = require('https');
import request = require('request');

import { Authenticator } from './auth';
import { User } from './config_types';

Expand All @@ -17,4 +20,8 @@ export class OpenIDConnectAuth implements Authenticator {
// TODO: Extract the 'Bearer ' to config.ts?
return `Bearer ${user.authProvider.config['id-token']}`;
}

public async applyAuthentication(user: User, opts: request.Options | https.RequestOptions) {
// pass
}
}

0 comments on commit b676607

Please sign in to comment.