Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.

Commit 1da74af

Browse files
authored
feat: Cache interactive login credentials (#222)
* 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
1 parent 3618646 commit 1da74af

10 files changed

+278
-70
lines changed

package-lock.json

Lines changed: 11 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/plugins/login/azureLoginPlugin.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe("Login Plugin", () => {
6868
undefined // would be options
6969
)
7070
expect(AzureLoginService.interactiveLogin).not.toBeCalled();
71-
expect(sls.variables["azureCredentials"]).toEqual(credentials);
71+
expect(JSON.stringify(sls.variables["azureCredentials"])).toEqual(JSON.stringify(credentials));
7272
expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
7373
});
7474

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import fs from "fs";
2+
import mockFs from "mock-fs";
3+
import { MockFactory } from "../../../test/mockFactory";
4+
import { SimpleFileTokenCache } from "./simpleFileTokenCache";
5+
6+
describe("Simple File Token Cache", () => {
7+
const tokenFilePath = "slsTokenCache.json";
8+
9+
let fileContent = {
10+
entries: [],
11+
subscriptions: [],
12+
};
13+
14+
afterEach(() => {
15+
mockFs.restore();
16+
});
17+
18+
it("Creates a load file on creation if none", () => {
19+
mockFs();
20+
21+
const writeFileSpy = jest.spyOn(fs, "writeFileSync");
22+
new SimpleFileTokenCache(tokenFilePath);
23+
24+
const expected = {
25+
entries: [],
26+
subscriptions: [],
27+
};
28+
29+
expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected));
30+
writeFileSpy.mockRestore();
31+
});
32+
33+
it("Load file on creation if available", () => {
34+
fileContent.entries = MockFactory.createTestTokenCacheEntries();
35+
36+
mockFs({
37+
"slsTokenCache.json": JSON.stringify(fileContent),
38+
});
39+
40+
const readFileSpy = jest.spyOn(fs, "readFileSync");
41+
const tokenCache = new SimpleFileTokenCache(tokenFilePath);
42+
43+
expect(readFileSpy).toBeCalled();
44+
expect(tokenCache.first()).not.toBeNull();
45+
readFileSpy.mockRestore();
46+
})
47+
48+
it("Saves to file after token is added", () => {
49+
mockFs();
50+
51+
const writeFileSpy = jest.spyOn(fs, "writeFileSync");
52+
const tokenCache = new SimpleFileTokenCache(tokenFilePath);
53+
const testEntries = MockFactory.createTestTokenCacheEntries();
54+
55+
tokenCache.add(testEntries);
56+
57+
const expected = {
58+
entries: testEntries,
59+
subscriptions: [],
60+
};
61+
62+
expect(tokenCache.isEmpty()).toBe(false);
63+
expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected));
64+
writeFileSpy.mockRestore();
65+
});
66+
67+
it("Saves to file after subscription is added", () => {
68+
mockFs();
69+
70+
const writeFileSpy = jest.spyOn(fs, "writeFileSync");
71+
const testFileCache = new SimpleFileTokenCache(tokenFilePath);
72+
const testSubs = MockFactory.createTestSubscriptions();
73+
74+
testFileCache.addSubs(testSubs);
75+
76+
const expected = {
77+
entries: [],
78+
subscriptions: testSubs,
79+
};
80+
81+
expect(writeFileSpy).toBeCalledWith(tokenFilePath, JSON.stringify(expected));
82+
writeFileSpy.mockRestore();
83+
});
84+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import os from "os";
4+
import * as adal from "adal-node";
5+
6+
const CONFIG_DIRECTORY = path.join(os.homedir(), ".azure");
7+
const DEFAULT_SLS_TOKEN_FILE = path.join(CONFIG_DIRECTORY, "slsTokenCache.json");
8+
9+
export class SimpleFileTokenCache implements adal.TokenCache {
10+
private entries: any[] = [];
11+
private subscriptions: any[] = [];
12+
13+
public constructor(private tokenPath: string = DEFAULT_SLS_TOKEN_FILE) {
14+
this.load();
15+
}
16+
17+
public add(entries: any[], callback?: (err?: Error, result?: boolean) => void) {
18+
this.entries.push(...entries);
19+
this.save();
20+
if (callback) {
21+
callback();
22+
}
23+
}
24+
25+
public remove(entries: any[], callback?: (err?: Error, result?: null) => void) {
26+
this.entries = this.entries.filter(e => {
27+
return !Object.keys(entries[0]).every(key => e[key] === entries[0][key]);
28+
});
29+
this.save();
30+
if (callback) {
31+
callback();
32+
}
33+
}
34+
35+
public find(query: any, callback: (err?: Error, result?: any[]) => void) {
36+
let result = this.entries.filter(e => {
37+
return Object.keys(query).every(key => e[key] === query[key]);
38+
});
39+
callback(null, result);
40+
return result;
41+
}
42+
43+
//-------- File toke cache specific methods
44+
45+
public addSubs(subscriptions: any) {
46+
this.subscriptions.push(...subscriptions);
47+
this.subscriptions = this.subscriptions.reduce((accumulator , current) => {
48+
const x = accumulator .find(item => item.id === current.id);
49+
if (!x) {
50+
return accumulator .concat([current]);
51+
} else {
52+
return accumulator ;
53+
}
54+
}, []);
55+
this.save();
56+
}
57+
58+
59+
public clear(callback: any) {
60+
this.entries = [];
61+
this.subscriptions = [];
62+
this.save();
63+
callback();
64+
}
65+
66+
public isEmpty() {
67+
return this.entries.length === 0;
68+
}
69+
70+
public first() {
71+
return this.entries[0];
72+
}
73+
74+
public listSubscriptions() {
75+
return this.subscriptions;
76+
}
77+
78+
private load() {
79+
if (fs.existsSync(this.tokenPath)) {
80+
let savedCache = JSON.parse(fs.readFileSync(this.tokenPath).toString());
81+
this.entries = savedCache.entries;
82+
this.entries.map(t => t.expiresOn = new Date(t.expiresOn))
83+
this.subscriptions = savedCache.subscriptions;
84+
} else {
85+
this.save();
86+
}
87+
}
88+
89+
public save() {
90+
fs.writeFileSync(this.tokenPath, JSON.stringify({ entries: this.entries, subscriptions: this.subscriptions }));
91+
}
92+
}

src/services/baseService.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,23 +129,24 @@ export abstract class BaseService {
129129
/**
130130
* Get the access token from credentials token cache
131131
*/
132-
protected getAccessToken(): string {
133-
return (this.credentials.tokenCache as any)._entries[0].accessToken;
132+
protected async getAccessToken(): Promise<string> {
133+
const token = await this.credentials.getToken();
134+
return token ? token.accessToken : null;
134135
}
135136

136137
/**
137138
* Sends an API request using axios HTTP library
138139
* @param method The HTTP method
139140
* @param relativeUrl The relative url
140-
* @param options Additional HTTP options including headers, etc
141+
* @param options Additional HTTP options including headers, etc.
141142
*/
142143
protected async sendApiRequest(
143144
method: string,
144145
relativeUrl: string,
145146
options: any = {}
146147
) {
147148
const defaultHeaders = {
148-
Authorization: `Bearer ${this.getAccessToken()}`
149+
Authorization: `Bearer ${await this.getAccessToken()}`
149150
};
150151

151152
const allHeaders = {

0 commit comments

Comments
 (0)