Skip to content

Commit

Permalink
feat: Cache interactive login credentials (#222)
Browse files Browse the repository at this point in the history
* Add simple file token cache

* Address pr feedback

* Added tests for existing and nonexisting cached login

* Cleaned up cached login test for login service

* Clean up simpleFileTokenCache, add tests

* Add param to simpleFileTokenCache constructor for path

* Updated simple file token cache tests

* Restore fs mocks at end of each test

* Fix getTokens in mock

* Fixing broken tests

* Address pr feedback

* Pushing again

* Address more pr feedback
  • Loading branch information
PIC123 authored Aug 8, 2019
1 parent 3618646 commit 1da74af
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 70 deletions.
41 changes: 11 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/plugins/login/azureLoginPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe("Login Plugin", () => {
undefined // would be options
)
expect(AzureLoginService.interactiveLogin).not.toBeCalled();
expect(sls.variables["azureCredentials"]).toEqual(credentials);
expect(JSON.stringify(sls.variables["azureCredentials"])).toEqual(JSON.stringify(credentials));
expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
});

Expand All @@ -78,7 +78,7 @@ describe("Login Plugin", () => {
await invokeLoginHook(false, sls);
expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled();
expect(AzureLoginService.interactiveLogin).toBeCalled();
expect(sls.variables["azureCredentials"]).toEqual(credentials);
expect(JSON.stringify(sls.variables["azureCredentials"])).toEqual(JSON.stringify(credentials));
expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
});

Expand Down
84 changes: 84 additions & 0 deletions src/plugins/login/utils/simpleFileTokenCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import fs from "fs";
import mockFs from "mock-fs";
import { MockFactory } from "../../../test/mockFactory";
import { SimpleFileTokenCache } from "./simpleFileTokenCache";

describe("Simple File Token Cache", () => {
const tokenFilePath = "slsTokenCache.json";

let fileContent = {
entries: [],
subscriptions: [],
};

afterEach(() => {
mockFs.restore();
});

it("Creates a load file on creation if none", () => {
mockFs();

const writeFileSpy = jest.spyOn(fs, "writeFileSync");
new SimpleFileTokenCache(tokenFilePath);

const expected = {
entries: [],
subscriptions: [],
};

expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected));
writeFileSpy.mockRestore();
});

it("Load file on creation if available", () => {
fileContent.entries = MockFactory.createTestTokenCacheEntries();

mockFs({
"slsTokenCache.json": JSON.stringify(fileContent),
});

const readFileSpy = jest.spyOn(fs, "readFileSync");
const tokenCache = new SimpleFileTokenCache(tokenFilePath);

expect(readFileSpy).toBeCalled();
expect(tokenCache.first()).not.toBeNull();
readFileSpy.mockRestore();
})

it("Saves to file after token is added", () => {
mockFs();

const writeFileSpy = jest.spyOn(fs, "writeFileSync");
const tokenCache = new SimpleFileTokenCache(tokenFilePath);
const testEntries = MockFactory.createTestTokenCacheEntries();

tokenCache.add(testEntries);

const expected = {
entries: testEntries,
subscriptions: [],
};

expect(tokenCache.isEmpty()).toBe(false);
expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected));
writeFileSpy.mockRestore();
});

it("Saves to file after subscription is added", () => {
mockFs();

const writeFileSpy = jest.spyOn(fs, "writeFileSync");
const testFileCache = new SimpleFileTokenCache(tokenFilePath);
const testSubs = MockFactory.createTestSubscriptions();

testFileCache.addSubs(testSubs);

const expected = {
entries: [],
subscriptions: testSubs,
};

expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected));
writeFileSpy.mockRestore();
});
});
92 changes: 92 additions & 0 deletions src/plugins/login/utils/simpleFileTokenCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import fs from "fs";
import path from "path";
import os from "os";
import * as adal from "adal-node";

const CONFIG_DIRECTORY = path.join(os.homedir(), ".azure");
const DEFAULT_SLS_TOKEN_FILE = path.join(CONFIG_DIRECTORY, "slsTokenCache.json");

export class SimpleFileTokenCache implements adal.TokenCache {
private entries: any[] = [];
private subscriptions: any[] = [];

public constructor(private tokenPath: string = DEFAULT_SLS_TOKEN_FILE) {
this.load();
}

public add(entries: any[], callback?: (err?: Error, result?: boolean) => void) {
this.entries.push(...entries);
this.save();
if (callback) {
callback();
}
}

public remove(entries: any[], callback?: (err?: Error, result?: null) => void) {
this.entries = this.entries.filter(e => {
return !Object.keys(entries[0]).every(key => e[key] === entries[0][key]);
});
this.save();
if (callback) {
callback();
}
}

public find(query: any, callback: (err?: Error, result?: any[]) => void) {
let result = this.entries.filter(e => {
return Object.keys(query).every(key => e[key] === query[key]);
});
callback(null, result);
return result;
}

//-------- File toke cache specific methods

public addSubs(subscriptions: any) {
this.subscriptions.push(...subscriptions);
this.subscriptions = this.subscriptions.reduce((accumulator , current) => {
const x = accumulator .find(item => item.id === current.id);
if (!x) {
return accumulator .concat([current]);
} else {
return accumulator ;
}
}, []);
this.save();
}


public clear(callback: any) {
this.entries = [];
this.subscriptions = [];
this.save();
callback();
}

public isEmpty() {
return this.entries.length === 0;
}

public first() {
return this.entries[0];
}

public listSubscriptions() {
return this.subscriptions;
}

private load() {
if (fs.existsSync(this.tokenPath)) {
let savedCache = JSON.parse(fs.readFileSync(this.tokenPath).toString());
this.entries = savedCache.entries;
this.entries.map(t => t.expiresOn = new Date(t.expiresOn))
this.subscriptions = savedCache.subscriptions;
} else {
this.save();
}
}

public save() {
fs.writeFileSync(this.tokenPath, JSON.stringify({ entries: this.entries, subscriptions: this.subscriptions }));
}
}
9 changes: 5 additions & 4 deletions src/services/baseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,23 +129,24 @@ export abstract class BaseService {
/**
* Get the access token from credentials token cache
*/
protected getAccessToken(): string {
return (this.credentials.tokenCache as any)._entries[0].accessToken;
protected async getAccessToken(): Promise<string> {
const token = await this.credentials.getToken();
return token ? token.accessToken : null;
}

/**
* Sends an API request using axios HTTP library
* @param method The HTTP method
* @param relativeUrl The relative url
* @param options Additional HTTP options including headers, etc
* @param options Additional HTTP options including headers, etc.
*/
protected async sendApiRequest(
method: string,
relativeUrl: string,
options: any = {}
) {
const defaultHeaders = {
Authorization: `Bearer ${this.getAccessToken()}`
Authorization: `Bearer ${await this.getAccessToken()}`
};

const allHeaders = {
Expand Down
Loading

0 comments on commit 1da74af

Please sign in to comment.