Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Cache interactive login credentials #222

Merged
merged 13 commits into from
Aug 8, 2019
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));
wbreza marked this conversation as resolved.
Show resolved Hide resolved
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, cb?: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's cb? a more descriptive name would be better

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a callback, I'll switch the name

this.entries.push(...entries);
this.save();
if (cb) {
cb();
}
}

public remove(entries: any, cb?: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callback

this.entries = this.entries.filter(e => {
return !Object.keys(entries[0]).every(key => e[key] === entries[0][key]);
});
this.save();
if (cb) {
cb();
}
}

public find(query: any, cb?: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

let result = this.entries.filter(e => {
return Object.keys(query).every(key => e[key] === query[key]);
});
cb(null, result);
return result;
}

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

public addSubs(entries: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does entries change? can we be more specific than just default to any?

this.subscriptions.push(...entries);
this.subscriptions = this.subscriptions.reduce((acc, current) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's acc stand for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's the accumulator for the reducer, not a very exciting name haha

const x = acc.find(item => item.id === current.id);
if (!x) {
return acc.concat([current]);
} else {
return acc;
}
}, []);
this.save();
}


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

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 }));
}
}
6 changes: 3 additions & 3 deletions src/services/baseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ 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> {
return this.credentials.getToken().then((result) => result.accessToken);
wbreza marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -139,7 +139,7 @@ export abstract class BaseService {
options: any = {}
) {
const defaultHeaders = {
Authorization: `Bearer ${this.getAccessToken()}`
Authorization: `Bearer ${await this.getAccessToken()}`
};

const allHeaders = {
Expand Down
6 changes: 3 additions & 3 deletions src/services/functionAppService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ describe("Function App Service", () => {
uri: expectedUploadUrl,
json: true,
headers: {
Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`,
Authorization: `Bearer ${(await variables["azureCredentials"].getToken()).accessToken}`,
Accept: "*/*",
ContentType: "application/octet-stream",
}
Expand Down Expand Up @@ -242,7 +242,7 @@ describe("Function App Service", () => {
uri: expectedUploadUrl,
json: true,
headers: {
Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`,
Authorization: `Bearer ${(await variables["azureCredentials"].getToken()).accessToken}`,
Accept: "*/*",
ContentType: "application/octet-stream",
}
Expand Down Expand Up @@ -276,7 +276,7 @@ describe("Function App Service", () => {
uri: expectedUploadUrl,
json: true,
headers: {
Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`,
Authorization: `Bearer ${(await variables["azureCredentials"].getToken()).accessToken}`,
Accept: "*/*",
ContentType: "application/octet-stream",
}
Expand Down
Loading