Skip to content

Commit 8607f60

Browse files
authored
Merge pull request #1890 from codefori/secure_password
Change secrets API
2 parents 98ce86a + 29cf23b commit 8607f60

File tree

5 files changed

+243
-69
lines changed

5 files changed

+243
-69
lines changed

src/api/Storage.ts

+44-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,22 @@ const LAST_PROFILE_KEY = `currentProfile`;
55
const SOURCE_LIST_KEY = `sourceList`;
66
const DEPLOYMENT_KEY = `deployment`;
77
const DEBUG_KEY = `debug`;
8-
const SERVER_SETTINGS_CACHE_KEY = (name : string) => `serverSettingsCache_${name}`;
8+
const SERVER_SETTINGS_CACHE_KEY = (name: string) => `serverSettingsCache_${name}`;
99
const PREVIOUS_SEARCH_TERMS_KEY = `prevSearchTerms`;
1010
const RECENTLY_OPENED_FILES_KEY = `recentlyOpenedFiles`;
11+
const AUTHORISED_EXTENSIONS_KEY = `authorisedExtensions`
1112

1213
export type PathContent = Record<string, string[]>;
1314
export type DeploymentPath = Record<string, string>;
1415
export type DebugCommands = Record<string, string>;
1516

17+
type AuthorisedExtension = {
18+
id: string
19+
displayName: string
20+
since: number
21+
lastAccess: number
22+
}
23+
1624
abstract class Storage {
1725
protected readonly globalState;
1826

@@ -180,7 +188,7 @@ export class ConnectionStorage extends Storage {
180188
return this.set(DEBUG_KEY, existingCommands);
181189
}
182190

183-
getWorkspaceDeployPath(workspaceFolder : vscode.WorkspaceFolder){
191+
getWorkspaceDeployPath(workspaceFolder: vscode.WorkspaceFolder) {
184192
const deployDirs = this.get<DeploymentPath>(DEPLOYMENT_KEY) || {};
185193
return deployDirs[workspaceFolder.uri.fsPath].toLowerCase();
186194
}
@@ -196,4 +204,38 @@ export class ConnectionStorage extends Storage {
196204
async clearRecentlyOpenedFiles() {
197205
await this.set(RECENTLY_OPENED_FILES_KEY, undefined);
198206
}
207+
208+
async grantExtensionAuthorisation(extension: vscode.Extension<any>) {
209+
const extensions = this.getAuthorisedExtensions();
210+
if (!this.getExtensionAuthorisation(extension)) {
211+
extensions.push({
212+
id: extension.id,
213+
displayName: extension.packageJSON.displayName,
214+
since: new Date().getTime(),
215+
lastAccess: new Date().getTime()
216+
});
217+
await this.set(AUTHORISED_EXTENSIONS_KEY, extensions);
218+
}
219+
}
220+
221+
getExtensionAuthorisation(extension: vscode.Extension<any>) {
222+
const authorisedExtension = this.getAuthorisedExtensions().find(authorisedExtension => authorisedExtension.id === extension.id);
223+
if (authorisedExtension) {
224+
authorisedExtension.lastAccess = new Date().getTime();
225+
}
226+
return authorisedExtension;
227+
}
228+
229+
getAuthorisedExtensions(): AuthorisedExtension[] {
230+
return this.get<AuthorisedExtension[]>(AUTHORISED_EXTENSIONS_KEY) || [];
231+
}
232+
233+
revokeAllExtensionAuthorisations() {
234+
this.revokeExtensionAuthorisation(...this.getAuthorisedExtensions());
235+
}
236+
237+
revokeExtensionAuthorisation(...extensions: AuthorisedExtension[]) {
238+
const newExtensions = this.getAuthorisedExtensions().filter(ext => !extensions.includes(ext));
239+
return this.set(AUTHORISED_EXTENSIONS_KEY, newExtensions);
240+
}
199241
}

src/instantiate.ts

+79-9
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ import Instance from "./api/Instance";
88
import { Search } from "./api/Search";
99
import { Terminal } from './api/Terminal';
1010
import { refreshDiagnosticsFromServer } from './api/errors/diagnostics';
11+
import { setupGitEventHandler } from './api/local/git';
1112
import { QSysFS, getUriFromPath, parseFSOptions } from "./filesystems/qsys/QSysFs";
1213
import { initGetNewLibl } from "./languages/clle/getnewlibl";
1314
import { SEUColorProvider } from "./languages/general/SEUColorProvider";
1415
import { Action, BrowserItem, DeploymentMethod, MemberItem, OpenEditableOptions, WithPath } from "./typings";
1516
import { SearchView } from "./views/searchView";
1617
import { ActionsUI } from './webviews/actions';
1718
import { VariablesUI } from "./webviews/variables";
18-
import { setupGitEventHandler } from './api/local/git';
1919

2020
export let instance: Instance;
2121

22+
const passwordAttempts: { [extensionId: string]: number } = {}
23+
2224
const CLEAR_RECENT = `$(trash) Clear recently opened`;
2325
const CLEAR_CACHED = `$(trash) Clear cached`;
2426

@@ -618,14 +620,82 @@ export async function loadAllofExtension(context: vscode.ExtensionContext) {
618620
}
619621
}),
620622

621-
vscode.commands.registerCommand(`code-for-ibmi.secret`, async (key: string, newValue: string) => {
622-
const connectionKey = `${instance.getConnection()!.currentConnectionName}_${key}`;
623-
if (newValue) {
624-
await context.secrets.store(connectionKey, newValue);
625-
return newValue;
626-
}
623+
vscode.commands.registerCommand(`code-for-ibmi.getPassword`, async (extensionId: string, reason?: string) => {
624+
if (extensionId) {
625+
const extension = vscode.extensions.getExtension(extensionId);
626+
const isValid = (extension && extension.isActive);
627+
if (isValid) {
628+
const connection = instance.getConnection();
629+
const storage = instance.getStorage();
630+
if (connection && storage) {
631+
const displayName = extension.packageJSON.displayName || extensionId;
632+
633+
// Some logic to stop spam from extensions.
634+
passwordAttempts[extensionId] = passwordAttempts[extensionId] || 0;
635+
if (passwordAttempts[extensionId] > 1) {
636+
throw new Error(`Password request denied for extension ${displayName}.`);
637+
}
638+
639+
const connectionKey = `${instance.getConnection()!.currentConnectionName}_password`;
640+
const storedPassword = await context.secrets.get(connectionKey);
627641

628-
return await context.secrets.get(connectionKey);
642+
if (storedPassword) {
643+
let isAuthed = storage.getExtensionAuthorisation(extension) !== undefined;
644+
645+
if (!isAuthed) {
646+
const detail = `The ${displayName} extension is requesting access to your password for this connection. ${reason ? `\n\nReason: ${reason}` : `The extension did not provide a reason for password access.`}`;
647+
let done = false;
648+
let modal = true;
649+
650+
while (!done) {
651+
const options: string[] = [`Allow`];
652+
653+
if (modal) {
654+
options.push(`View on Marketplace`);
655+
} else {
656+
options.push(`Deny`);
657+
}
658+
659+
const result = await vscode.window.showWarningMessage(
660+
modal ? `Password Request` : detail,
661+
{
662+
modal,
663+
detail,
664+
},
665+
...options
666+
);
667+
668+
switch (result) {
669+
case `Allow`:
670+
await storage.grantExtensionAuthorisation(extension);
671+
isAuthed = true;
672+
done = true;
673+
break;
674+
675+
case `View on Marketplace`:
676+
vscode.commands.executeCommand('extension.open', extensionId);
677+
modal = false;
678+
break;
679+
680+
default:
681+
done = true;
682+
break;
683+
}
684+
}
685+
}
686+
687+
if (isAuthed) {
688+
return storedPassword;
689+
} else {
690+
passwordAttempts[extensionId]++;
691+
}
692+
}
693+
694+
} else {
695+
throw new Error(`Not connected to an IBM i.`);
696+
}
697+
}
698+
}
629699
}),
630700

631701
vscode.commands.registerCommand("code-for-ibmi.browse", (item: WithPath | MemberItem) => {
@@ -660,7 +730,7 @@ export async function loadAllofExtension(context: vscode.ExtensionContext) {
660730
}
661731

662732
// Register git events based on workspace folders
663-
if (vscode.workspace.workspaceFolders) {
733+
if (vscode.workspace.workspaceFolders) {
664734
setupGitEventHandler(context);
665735
}
666736

src/testing/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ContentSuite } from "./content";
77
import { DeployToolsSuite } from "./deployTools";
88
import { FilterSuite } from "./filter";
99
import { ILEErrorSuite } from "./ileErrors";
10+
import { StorageSuite } from "./storage";
1011
import { TestSuitesTreeProvider } from "./testCasesTree";
1112
import { ToolsSuite } from "./tools";
1213

@@ -17,7 +18,8 @@ const suites: TestSuite[] = [
1718
DeployToolsSuite,
1819
ToolsSuite,
1920
ILEErrorSuite,
20-
FilterSuite
21+
FilterSuite,
22+
StorageSuite
2123
]
2224

2325
export type TestSuite = {

src/testing/storage.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import assert from "assert";
2+
import vscode from "vscode";
3+
import { TestSuite } from ".";
4+
import { instance } from "../instantiate";
5+
6+
export const StorageSuite: TestSuite = {
7+
name: `Extension storage tests`,
8+
tests: [
9+
{
10+
name: "Authorized extensions", test: async () => {
11+
const storage = instance.getStorage();
12+
if(storage){
13+
const extension = vscode.extensions.getExtension("halcyontechltd.code-for-ibmi")!;
14+
try{
15+
let auth = storage.getExtensionAuthorisation(extension);
16+
assert.strictEqual(undefined, auth, "Extension is already authorized");
17+
storage.grantExtensionAuthorisation(extension);
18+
19+
auth = storage.getExtensionAuthorisation(extension);
20+
assert.ok(auth, "Authorisation not found");
21+
assert.strictEqual(new Date(auth.since).toDateString(), new Date().toDateString(), "Access date must be today");
22+
23+
const lastAccess = auth.lastAccess;
24+
await new Promise(r => setTimeout(r, 100)); //Wait a bit
25+
auth = storage.getExtensionAuthorisation(extension);
26+
assert.ok(auth, "Authorisation not found");
27+
assert.notStrictEqual(lastAccess, auth.lastAccess, "Last access did not change")
28+
}
29+
finally{
30+
const auth = storage.getExtensionAuthorisation(extension);
31+
if(auth){
32+
storage.revokeExtensionAuthorisation(auth);
33+
}
34+
}
35+
}
36+
else{
37+
throw Error("Cannot run test: no storage")
38+
}
39+
}
40+
}
41+
]
42+
}

0 commit comments

Comments
 (0)