diff --git a/.vscodeignore b/.vscodeignore index ede1991..3c53c64 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -19,6 +19,9 @@ webpack.config.js node_modules # Required to render the Webview UI Toolkit, see src/webviews/utils.ts !node_modules/@vscode/webview-ui-toolkit/dist/toolkit.js +# Required to embed codicons in webviews, see srv/webviews/utils.ts +!node_modules/@vscode/codicons/dist/codicon.css +!node_modules/@vscode/codicons/dist/codicon.ttf # Required because ldapjs does not support webpack, see https://github.com/ldapjs/node-ldapjs/issues/421 !node_modules/ldapjs # ldapjs dependencies (direct and indirect), see node_modules/ldapjs/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index aba052c..4558e52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - New setting `ldap-explorer.sort-attributes` allows to order attributes alphabetically by name +- Bind passwords can now be stored encrypted in secret storage or prompted at connection time ; this is configurable via a new field labeled "Bind Password mode" on the connection edit screen ([#64](https://github.com/fengtan/ldap-explorer/issues/64)) +- Button to reveal or hide bind passwords ([#64](https://github.com/fengtan/ldap-explorer/issues/64)) ## [1.4.1] - 2025-01-13 ### Added diff --git a/README.md b/README.md index 2e96526..8713144 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ * **Export LDAP entries as CSV** - Share and analyze LDAP results using a standard CSV format * **Manage bookmarks** - Bookmark LDAP entries you often need to check or located in awkward places * **Support for multiple connections** - Manage multiple LDAP connections, such as a test and a production connections -* **Support for environment variables** - Easy integration with containers and increased security: you don't have to store your bind credentials unencrypted in VS Code settings +* **Support for environment variables** - Easy integration with containers +* **Secure credentials** - Bind passwords may be stored encrypted in secret storage, or not stored at all and requested at connection time ## Demo @@ -117,7 +118,8 @@ List of LDAP connections. Example: "host": "acme.example.net", "port": "389", "binddn": "cn=admin,dc=example,dc=org", - "bindpwd": "foobar", + "pwdmode": "settings", + "bindpwd": "foobar", // Only applicable if "pwdmode" is "settings" "basedn": "dc=example,dc=org", "limit": "0", "paged": "true", @@ -131,6 +133,11 @@ List of LDAP connections. Example: } ``` +Supported values for `pwdmode`: +- `secret` will read the bind password from secret storage (encrypted) +- `ask` will ask for the bind password at connection time +- `settings` will read the bind password as plaint text from settings (connection attribute `bindpwd`) + * **ldap-explorer.show-tree-item-icons** (`false`) If set to `true`, LDAP entries in the Tree view will be rendered with an icon based on their entity type: diff --git a/assets/js/createAddEditConnectionWebview.js b/assets/js/createAddEditConnectionWebview.js index 7acd693..6b75de0 100644 --- a/assets/js/createAddEditConnectionWebview.js +++ b/assets/js/createAddEditConnectionWebview.js @@ -14,6 +14,7 @@ function submitForm(command) { host: document.getElementById("host").value, port: document.getElementById("port").value, binddn: document.getElementById("binddn").value, + pwdmode: document.getElementById("pwdmode").value, bindpwd: document.getElementById("bindpwd").value, basedn: document.getElementById("basedn").value, limit: document.getElementById("limit").value, @@ -23,9 +24,28 @@ function submitForm(command) { }); } +// Reveal / hide the bind password field. +function toggleBindPwdVisibility() { + const type = document.getElementById("bindpwd").type; + document.getElementById("bindpwd").type = (type === "password") + ? "text" + : "password"; + updateBindPwdIcon(); +} + +// If bind password is revealed, show button with icon to hide it. +// If it is hidden, show button with icon to reveal it. +function updateBindPwdIcon() { + const type = document.getElementById("bindpwd").type; + document.getElementById("bindpwd-toggle").className = (type === "password") + ? "codicon codicon-eye" + : "codicon codicon-eye-closed"; +} + function updateFieldsVisibility() { var protocol = document.getElementById("protocol").value; var starttls = document.getElementById("starttls").checked; + var pwdmode = document.getElementById("pwdmode").value; // Show StartTLS checkbox only if drop-down Protocol is set to "ldap". document.getElementById("starttls").style["display"] = (protocol === "ldap") ? "" : "none"; @@ -34,9 +54,15 @@ function updateFieldsVisibility() { // 1. Protocol is set to "ldaps" // 2. Protocol is set to "ldap" and StartTLS checkbox is checked document.getElementById("tlsoptions").style["display"] = ((protocol === "ldaps") || (protocol === "ldap" && starttls)) ? "" : "none"; + + // Show Bind Password field only if password mode is "secret" or "settings" + // (there is no ned to ask for a password if the mode is "ask" or "anonymous"). + // See PasswordMode.ts. + document.getElementById("bindpwd-container").style["display"] = ((pwdmode === "secret") || (pwdmode === "settings")) ? "" : "none"; } -// Initialize fields visibility when loading the webview. +// Initialize fields visibility & icons when loading the webview. document.addEventListener('DOMContentLoaded', function () { updateFieldsVisibility(); + updateBindPwdIcon(); }, false); diff --git a/package-lock.json b/package-lock.json index 6d52c6f..271bba3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.4.1", "license": "GPL-3.0-only", "dependencies": { + "@vscode/codicons": "^0.0.36", "@vscode/webview-ui-toolkit": "^1.0.1", "ldapjs": "^2.3.3" }, @@ -546,6 +547,11 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vscode/codicons": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.36.tgz", + "integrity": "sha512-wsNOvNMMJ2BY8rC2N2MNBG7yOowV3ov8KlvUE/AiVUlHKTfWsw3OgAOQduX7h0Un6GssKD3aoTVH+TF3DSQwKQ==" + }, "node_modules/@vscode/test-electron": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.1.tgz", diff --git a/package.json b/package.json index 720f5e2..c72c6c2 100644 --- a/package.json +++ b/package.json @@ -447,6 +447,7 @@ "webpack-cli": "^4.10.0" }, "dependencies": { + "@vscode/codicons": "^0.0.36", "@vscode/webview-ui-toolkit": "^1.0.1", "ldapjs": "^2.3.3" } diff --git a/src/LdapConnection.ts b/src/LdapConnection.ts index a27421e..8c9c488 100644 --- a/src/LdapConnection.ts +++ b/src/LdapConnection.ts @@ -1,8 +1,9 @@ import { readFileSync } from 'fs'; -import { window, workspace } from 'vscode'; +import { ExtensionContext, window, workspace } from 'vscode'; import { Attribute, Client, createClient, SearchEntry, SearchOptions } from 'ldapjs'; import { LdapLogger } from './LdapLogger'; import { CACertificateManager } from './CACertificateManager'; +import { PasswordMode } from './PasswordMode'; /** * Represents an LDAP connection. @@ -21,7 +22,8 @@ export class LdapConnection { private host: string; private port: string; private binddn: string; - private bindpwd: string; + private pwdmode: string; + private bindpwd: string | undefined; private basedn: string; private limit: string; private paged: string; @@ -29,7 +31,24 @@ export class LdapConnection { private timeout: string; private bookmarks: string[]; - constructor( + constructor({ + name, + protocol, + starttls, + verifycert, + sni, + host, + port, + binddn, + pwdmode, + bindpwd, + basedn, + limit, + paged, + connectTimeout, + timeout, + bookmarks, + }: { name: string, protocol: string, starttls: string, @@ -38,14 +57,15 @@ export class LdapConnection { host: string, port: string, binddn: string, + pwdmode: string, bindpwd: string, basedn: string, limit: string, paged: string, connectTimeout: string, timeout: string, - bookmarks: string[] - ) { + bookmarks: string[], + }) { this.name = name; this.protocol = protocol; this.starttls = starttls; @@ -54,6 +74,7 @@ export class LdapConnection { this.host = host; this.port = port; this.binddn = binddn; + this.pwdmode = pwdmode; this.bindpwd = bindpwd; this.basedn = basedn; this.limit = limit; @@ -89,8 +110,16 @@ export class LdapConnection { return this.get(this.binddn, evaluate); } public getBindPwd(evaluate: boolean) { + // If bindpwd is undefined then return empty string. + if (!this.bindpwd) { + return ""; + } + // Otherwise return the value and evaluate it, if necessary. return this.get(this.bindpwd, evaluate); } + public getPwdMode(evaluate: boolean) { + return this.get(this.pwdmode, evaluate); + } public getBaseDn(evaluate: boolean) { return this.get(this.basedn, evaluate); } @@ -144,6 +173,13 @@ export class LdapConnection { this.name = name; } + /** + * Set the bind password of the connection. + */ + public setBindPwd(bindpwd: string | undefined) { + this.bindpwd = bindpwd; + } + /** * Adds a bookmark. */ @@ -221,11 +257,19 @@ export class LdapConnection { * - Pass a callback onSearchEntryFound (will fire for *each* result as they are received) * - Resolve callback - this function is Thenable (will fire when *all* results have been received) */ - public search( + public search({ + context, + searchOptions, + pwdmode, + base, + onSearchEntryFound, + }: { + context: ExtensionContext, searchOptions: SearchOptions, - base: string = this.getBaseDn(true), - onSearchEntryFound?: (entry: SearchEntry) => void - ): Thenable { + pwdmode?: string, + base?: string, + onSearchEntryFound?: (entry: SearchEntry) => void, + }): Thenable { return new Promise((resolve, reject) => { // Get ldapjs client. const client: Client = createClient({ @@ -249,29 +293,88 @@ export class LdapConnection { if (err) { return reject(`Unable to initiate StartTLS: ${err.message}`); } - return this.getResults(client, resolve, reject, searchOptions, base, onSearchEntryFound); + return this.getResults({ + context: context, + client: client, + resolve: resolve, + reject: reject, + searchOptions: searchOptions, + pwdmode: pwdmode, + base: base, + onSearchEntryFound: onSearchEntryFound, + }); }); } else { - return this.getResults(client, resolve, reject, searchOptions, base, onSearchEntryFound); + return this.getResults({ + context: context, + client: client, + resolve: resolve, + reject: reject, + searchOptions: searchOptions, + pwdmode: pwdmode, + base: base, + onSearchEntryFound: onSearchEntryFound, + }); } }); } + /** + * Opens input box asking the user to enter a bind password. + */ + protected async pickBindPassword(): Promise { + return await window.showInputBox({ prompt: `Bind password for connection "${this.getName()}"` }); + } + /** * Get results from LDAP servers. */ - protected getResults( + protected async getResults({ + context, + client, + resolve, + reject, + searchOptions, + pwdmode, + base, + onSearchEntryFound, + }: { + context: ExtensionContext, client: Client, resolve: (value: SearchEntry[] | PromiseLike) => void, reject: (reason?: any) => void, searchOptions: SearchOptions, - base: string = this.getBaseDn(true), + // Override password mode configured for this connection. + // Essentially used when testing the connection (in which case the bind password may not yet be persisted in settings or secret store). + pwdmode?: string, + // Override base DN. + // Essentially used to search for immeditate children in tree view. + base?: string, onSearchEntryFound ?: (entry: SearchEntry) => void - ) { + }) { + // Get bind password depending on password mode. + let bindpwd: string | undefined; + switch (pwdmode ?? this.getPwdMode(true)) { + case PasswordMode.ask: + bindpwd = await this.pickBindPassword(); + if (!bindpwd) { + return reject("No bind password was provided."); + } + break; + case PasswordMode.secretStorage: + bindpwd = await context.secrets.get(this.getName()) ?? ""; + break; + case PasswordMode.settings: + default: + // Default option (i.e. when the connection has no password mode) = read + // password from VS Code settings. This was the only storage mode + // available before this extension started to support password modes. + bindpwd = this.getBindPwd(true); + } // Bind. - client.bind(this.getBindDn(true), this.getBindPwd(true), (err) => { + client.bind(this.getBindDn(true), bindpwd, (err) => { if (err) { return reject(`Unable to bind: ${err.message}`); } @@ -279,7 +382,7 @@ export class LdapConnection { // Search. searchOptions.sizeLimit = parseInt(this.getLimit(true)); try { - client.search(base, searchOptions, (err, res) => { + client.search(base ?? this.getBaseDn(true), searchOptions, (err, res) => { if (err) { return reject(err.message); } diff --git a/src/LdapConnectionManager.ts b/src/LdapConnectionManager.ts index 85e94e4..94e8a15 100644 --- a/src/LdapConnectionManager.ts +++ b/src/LdapConnectionManager.ts @@ -1,35 +1,43 @@ -import { ExtensionContext, workspace } from 'vscode'; +import { ExtensionContext, SecretStorage, window, workspace } from 'vscode'; import { LdapConnection } from './LdapConnection'; import { LdapLogger } from './LdapLogger'; +import { PasswordMode } from './PasswordMode'; /** * Manages storage of connections in VS Code settings. */ export class LdapConnectionManager { + private context: ExtensionContext; + + public constructor(context: ExtensionContext) { + this.context = context; + } + /** * Get all connections stored in VS Code settings. */ - public static getConnections(): LdapConnection[] { - return workspace.getConfiguration('ldap-explorer').get('connections', []).map(connection => new LdapConnection( + public getConnections(): LdapConnection[] { + return workspace.getConfiguration('ldap-explorer').get('connections', []).map(connection => new LdapConnection({ // Same default values as what is listed in package.json. // Providing default values brings backwards compatibility when adding more attributes. - connection["name"], - connection["protocol"] || "ldap", - connection["starttls"] || "false", - connection["verifycert"] || "true", - connection["sni"] || "", - connection["host"] || "", - connection["port"] || "", - connection["binddn"] || "", - connection["bindpwd"] || "", - connection["basedn"] || "", - connection["limit"] || "0", - connection["paged"] || "true", - connection["connectTimeout"] || "5000", - connection["timeout"] || "5000", - connection["bookmarks"] || [] - )); + name: connection["name"], + protocol: connection["protocol"] || "ldap", + starttls: connection["starttls"] || "false", + verifycert: connection["verifycert"] || "true", + sni: connection["sni"] || "", + host: connection["host"] || "", + port: connection["port"] || "", + binddn: connection["binddn"] || "", + pwdmode: connection["pwdmode"] || "settings", // Default to settings if pwd mode is not set for backwards compatibility. + bindpwd: connection["bindpwd"] || "", + basedn: connection["basedn"] || "", + limit: connection["limit"] || "0", + paged: connection["paged"] || "true", + connectTimeout: connection["connectTimeout"] || "5000", + timeout: connection["timeout"] || "5000", + bookmarks: connection["bookmarks"] || [], + })); } /** @@ -37,7 +45,7 @@ export class LdapConnectionManager { * * Returns 'undefined' if no connection with such a name was found. */ - public static getConnection(name: string): LdapConnection | undefined { + public getConnection(name: string): LdapConnection | undefined { const filteredConnections = this.getConnections().filter(connection => connection.getName() === name); if (filteredConnections.length < 1) { return undefined; @@ -51,46 +59,80 @@ export class LdapConnectionManager { /** * Set the active connection. */ - public static setActiveConnection(context: ExtensionContext, connection: LdapConnection): Thenable { - return context.globalState.update('active-connection', connection.getName()); + public setActiveConnection(connection: LdapConnection): Thenable { + return this.context.globalState.update('active-connection', connection.getName()); } /** * Sets no active connection. */ - public static setNoActiveConnection(context: ExtensionContext): Thenable { - return context.globalState.update('active-connection', undefined); + public setNoActiveConnection(): Thenable { + return this.context.globalState.update('active-connection', undefined); } /** * Get the currently active connection. */ - public static getActiveConnection(context: ExtensionContext): LdapConnection | undefined { - const connectionName: string | undefined = context.globalState.get('active-connection'); + public getActiveConnection(): LdapConnection | undefined { + const connectionName: string | undefined = this.context.globalState.get('active-connection'); if (connectionName === undefined) { return undefined; } return this.getConnection(connectionName); } + /** + * Update a bind password in secret storage. + */ + public updateBindPwdInSecretStorage(connection: LdapConnection) { + // The SecretStorage API automatically takes care of namespacing so we don't + // need to worry about collistions with other extensions. + return this.context.secrets.store(connection.getName(), connection.getBindPwd(false)); + } + + /** + * Remove a bind password from secret store. + */ + public deleteBindPwdFromSecretStorage(connection: LdapConnection) { + return this.context.secrets.delete(connection.getName()); + } + /** * Add a new connection to settings. */ - public static addConnection(connection: LdapConnection): Thenable { + public async addConnection(connection: LdapConnection): Promise { // Get list of existing connections. let connections = this.getConnections(); + // Update or delete password in secret storage. + if (connection.getPwdMode(true) === PasswordMode.secretStorage) { + await this.updateBindPwdInSecretStorage(connection); + } else { + await this.deleteBindPwdFromSecretStorage(connection); + } + // Add the new connection. + // If password mode is different from "settings" then we don't want to + // persist it here so we temporarily remove it from the connection object. + const bindpwd = connection.getBindPwd(false); + if (connection.getPwdMode(true) !== PasswordMode.settings) { + connection.setBindPwd(undefined); + } connections.push(connection); // Save new list of connections and return Thenable. - return workspace.getConfiguration('ldap-explorer').update('connections', connections, true); + // Reinstate bind password is password mode is different from "settings". + return workspace.getConfiguration('ldap-explorer').update('connections', connections, true).then(() => { + if (connection.getPwdMode(true) !== PasswordMode.settings) { + connection.setBindPwd(bindpwd); + } + }); } /** * Update an existing connection in settings. */ - public static editConnection(newConnection: LdapConnection, existingConnectionName: string): Thenable { + public async editConnection(newConnection: LdapConnection, existingConnectionName: string): Promise { // Get list of existing connections. let connections = this.getConnections(); @@ -100,17 +142,43 @@ export class LdapConnectionManager { return Promise.reject(`Connection '${existingConnectionName}' does not exist in settings`); } + // Update or delete password in secret storage. + if (newConnection.getPwdMode(true) === PasswordMode.secretStorage) { + // Default value of bind password text field is empty for security reasons. + // Hence we only update the password in secret storage only if the user + // actually provided a new password. + // See createAddEditConnectionWebview.ts. + if (newConnection.getBindPwd(false) !== "") { + await this.updateBindPwdInSecretStorage(newConnection); + } + } else { + await this.deleteBindPwdFromSecretStorage(newConnection); + } + // Replace existing connection with new connection. + // If password mode is different from "settings" then we don't want to + // persist it here so we temporarily remove it from the connection object. + const bindpwd = newConnection.getBindPwd(false); + if (newConnection.getPwdMode(true) !== PasswordMode.settings) { + newConnection.setBindPwd(undefined); + } connections[index] = newConnection; // Save new list of connections and return Thenable. - return workspace.getConfiguration('ldap-explorer').update('connections', connections, true); + // Reinstate bind password is password mode is different from "settings". + return workspace.getConfiguration('ldap-explorer').update('connections', connections, true).then(() => { + if (newConnection.getPwdMode(true) !== PasswordMode.settings) { + newConnection.setBindPwd(bindpwd); + } + }); } /** * Remove an existing connection from settings. + * + * Also removes the password from secret storage. */ - public static removeConnection(connection: LdapConnection): Thenable { + public async removeConnection(connection: LdapConnection): Promise { // Get list of existing connections. const connections = this.getConnections(); @@ -123,6 +191,9 @@ export class LdapConnectionManager { // Remove connection from the list. connections.splice(index, 1); + // Remove password from secret store (regardless of current password mode). + await this.deleteBindPwdFromSecretStorage(connection); + // Save new list of connections and return Thenable. return workspace.getConfiguration('ldap-explorer').update('connections', connections, true); } diff --git a/src/PasswordMode.ts b/src/PasswordMode.ts new file mode 100644 index 0000000..777c0eb --- /dev/null +++ b/src/PasswordMode.ts @@ -0,0 +1,14 @@ +export enum PasswordMode { + + // Store bind password encrypted in VS Code secret store. + // This member value is also hardcoded in createAddEditConnectionWebview.js. + secretStorage = "secret", + + // Store bind password as plain text in VS Code settings. + // This member value is also hardcoded in createAddEditConnectionWebview.js. + settings = "settings", + + // Ask for bind password everytime a connection is made. + ask = "ask", + +} diff --git a/src/extension.ts b/src/extension.ts index ebc4068..e1d4b8c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,25 +15,27 @@ import { CACertificateManager } from './CACertificateManager'; */ export function activate(context: ExtensionContext) { + const connectionManager = new LdapConnectionManager(context); + // Create our views (connections, tree, bookmarks, search). const cacertTreeDataProvider = new CACertificateTreeDataProvider(); const cacertTreeView = window.createTreeView('ldap-explorer-view-cacerts', { treeDataProvider: cacertTreeDataProvider }); context.subscriptions.push(cacertTreeView); - const connectionTreeDataProvider = new ConnectionTreeDataProvider(context); + const connectionTreeDataProvider = new ConnectionTreeDataProvider(connectionManager); const connectionTreeView = window.createTreeView('ldap-explorer-view-connections', { treeDataProvider: connectionTreeDataProvider }); context.subscriptions.push(connectionTreeView); - const entryTreeDataProvider = new EntryTreeDataProvider(context); + const entryTreeDataProvider = new EntryTreeDataProvider(context, connectionManager); const entryTreeView = window.createTreeView('ldap-explorer-view-tree', { treeDataProvider: entryTreeDataProvider }); context.subscriptions.push(entryTreeView); - const bookmarkTreeDataProvider = new BookmarkTreeDataProvider(context); + const bookmarkTreeDataProvider = new BookmarkTreeDataProvider(connectionManager); const bookmarkTreeView = window.createTreeView('ldap-explorer-view-bookmarks', { treeDataProvider: bookmarkTreeDataProvider }); context.subscriptions.push(bookmarkTreeView); - const searchWebviewViewProvider = new SearchWebviewViewProvider(context); + const searchWebviewViewProvider = new SearchWebviewViewProvider(context, connectionManager); context.subscriptions.push( window.registerWebviewViewProvider('ldap-explorer-view-search', searchWebviewViewProvider, { webviewOptions: { retainContextWhenHidden: true } }) ); @@ -103,14 +105,14 @@ export function activate(context: ExtensionContext) { })); context.subscriptions.push(commands.registerCommand('ldap-explorer.add-connection', () => { - createAddEditConnectionWebview(context); + createAddEditConnectionWebview(context, connectionManager); })); context.subscriptions.push(commands.registerCommand('ldap-explorer.edit-connection', async (connection?: LdapConnection) => { // connection may not be defined (e.g. if the command fired from the command palette instead of the tree view). // If that is the case we explictly ask the user to pick a connection. if (!connection) { - connection = await pickConnection(); + connection = await pickConnection(connectionManager); // User did not provide a connection: cancel command. if (!connection) { return; @@ -119,17 +121,17 @@ export function activate(context: ExtensionContext) { // Reload connection details from settings. // Ensures the connection object includes bookmarks. - connection = LdapConnectionManager.getConnection(connection.getName()); + connection = connectionManager.getConnection(connection.getName()); // Create webview to edit connection. - createAddEditConnectionWebview(context, connection); + createAddEditConnectionWebview(context, connectionManager, connection); })); context.subscriptions.push(commands.registerCommand('ldap-explorer.delete-connection', async (connection?: LdapConnection) => { // connection may not be defined (e.g. if the command fired from the command palette instead of the tree view). // If that is the case we explictly ask the user to pick a connection. if (!connection) { - connection = await pickConnection(); + connection = await pickConnection(connectionManager); // User did not provide a connection: cancel command. if (!connection) { return; @@ -137,14 +139,14 @@ export function activate(context: ExtensionContext) { } // Remove connection. - askAndRemoveConnection(connection); + askAndRemoveConnection(connectionManager, connection); })); context.subscriptions.push(commands.registerCommand('ldap-explorer.activate-connection', async (connection?: LdapConnection) => { // connection may not be defined (e.g. if the command fired from the command palette instead of the tree view). // If that is the case we explictly ask the user to pick a connection. if (!connection) { - connection = await pickConnection(); + connection = await pickConnection(connectionManager); // User did not provide a connection: cancel command. if (!connection) { return; @@ -152,7 +154,7 @@ export function activate(context: ExtensionContext) { } // Store name of new active connection in Memento. - LdapConnectionManager.setActiveConnection(context, connection).then(() => { + connectionManager.setActiveConnection(connection).then(() => { // Refresh views so the new active connection shows up. commands.executeCommand("ldap-explorer.refresh"); }); @@ -162,7 +164,7 @@ export function activate(context: ExtensionContext) { context.subscriptions.push(commands.registerCommand('ldap-explorer.deactivate-connection', () => { // Set no active connection. - LdapConnectionManager.setNoActiveConnection(context).then(() => { + connectionManager.setNoActiveConnection().then(() => { // Refresh views so the new active connection shows up. commands.executeCommand("ldap-explorer.refresh"); }); @@ -185,7 +187,7 @@ export function activate(context: ExtensionContext) { context.subscriptions.push(commands.registerCommand('ldap-explorer.show-attributes', async (dn?: string) => { // If there is no active connection, then explicitly ask user to pick one. - const connection = LdapConnectionManager.getActiveConnection(context) ?? await pickConnection(); + const connection = connectionManager.getActiveConnection() ?? await pickConnection(connectionManager); // User did not provide a connection: cancel command. if (!connection) { @@ -208,7 +210,7 @@ export function activate(context: ExtensionContext) { context.subscriptions.push(commands.registerCommand('ldap-explorer.reveal-in-tree', async (dn?: string) => { // If there is no active connection, then ask user to pick one. - if (!LdapConnectionManager.getActiveConnection(context)) { + if (!connectionManager.getActiveConnection()) { const connection = await commands.executeCommand("ldap-explorer.activate-connection"); if (!connection) { // User did not provide a connection: cancel command. @@ -240,7 +242,7 @@ export function activate(context: ExtensionContext) { context.subscriptions.push(commands.registerCommand('ldap-explorer.add-bookmark', async (dn?: string) => { // If there is no active connection, then explicitly ask user to pick one. - const connection = LdapConnectionManager.getActiveConnection(context) ?? await pickConnection(); + const connection = connectionManager.getActiveConnection() ?? await pickConnection(connectionManager); // User did not provide a connection: cancel command. if (!connection) { @@ -261,7 +263,7 @@ export function activate(context: ExtensionContext) { connection.addBookmark(dn); // Persist bookmark in connection. - LdapConnectionManager.editConnection(connection, connection.getName()).then( + connectionManager.editConnection(connection, connection.getName()).then( value => { // If the connection was successfully updated, then refresh the // bookmarks view so the new bookmark shows up. @@ -276,7 +278,7 @@ export function activate(context: ExtensionContext) { context.subscriptions.push(commands.registerCommand('ldap-explorer.delete-bookmark', async (dn?: string) => { // If there is no active connection, then explicitly ask user to pick one. - const connection = LdapConnectionManager.getActiveConnection(context) ?? await pickConnection(); + const connection = connectionManager.getActiveConnection() ?? await pickConnection(connectionManager); // User did not provide a connection: cancel command. if (!connection) { @@ -297,7 +299,7 @@ export function activate(context: ExtensionContext) { connection.deleteBookmark(dn); // Persist removal of the bookmark from the connection. - LdapConnectionManager.editConnection(connection, connection.getName()).then( + connectionManager.editConnection(connection, connection.getName()).then( value => { // If the connection was successfully updated, then refresh the // bookmarks view so the bookmark goes away. @@ -340,8 +342,8 @@ async function pickExistingCACert(): Promise { /** * Opens quick pick box asking the user to select a connection. */ -async function pickConnection(): Promise { - const options = LdapConnectionManager.getConnections().map(connection => { +async function pickConnection(connectionManager: LdapConnectionManager): Promise { + const options = connectionManager.getConnections().map(connection => { return { label: connection.getName(), description: connection.getUrl(), @@ -356,16 +358,16 @@ async function pickConnection(): Promise { } // Otherwise return connection object. - return LdapConnectionManager.getConnection(option.name); + return connectionManager.getConnection(option.name); } /** * Ask for a confirmation and actually remove a connection from settings. */ -function askAndRemoveConnection(connection: LdapConnection) { +function askAndRemoveConnection(connectionManager: LdapConnectionManager, connection: LdapConnection) { window.showInformationMessage(`Are you sure you want to remove the connection '${connection.getName()} ?`, { modal: true }, "Yes").then(confirm => { if (confirm) { - LdapConnectionManager.removeConnection(connection).then( + connectionManager.removeConnection(connection).then( value => { // If connection was successfully removed, refresh tree view so it does not show up anymore. commands.executeCommand("ldap-explorer.refresh"); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 9c97026..a5f191f 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -6,46 +6,48 @@ suite('Extension test suite', () => { test('Test connection string', () => { // Create dummy connection. - const connection: LdapConnection = new LdapConnection( - "my connection", - "ldap", - "false", - "true", - "", - "myserver.com", - "1389", - "cn=admin,dc=example,dc=org", - "foobar", - "dc=example,dc=org", - "0", - "true", - "5000", - "5000", - [] - ); + const connection: LdapConnection = new LdapConnection({ + name: "my connection", + protocol: "ldap", + starttls: "false", + verifycert: "true", + sni: "", + host: "myserver.com", + port: "1389", + binddn: "cn=admin,dc=example,dc=org", + pwdmode: "settings", + bindpwd: "foobar", + basedn: "dc=example,dc=org", + limit: "0", + paged: "true", + connectTimeout: "5000", + timeout: "5000", + bookmarks: [], + }); // Assert connection string. assert.strictEqual("ldap://myserver.com:1389", connection.getUrl()); }); test('Test environment variables', () => { // Create connection which parameters are defined with environment variables. - const connection: LdapConnection = new LdapConnection( - "my connection with env vars", - "${protocol}", - "${starttls}", - "${verifycert}", - "${sni}", - "${host}", - "${port}", - "${binddn}", - "${bindpwd}", - "${basedn}", - "${sizelimit}", - "${paged}", - "${connectTimeout}", - "${timeout}", - [] - ); + const connection: LdapConnection = new LdapConnection({ + name: "my connection with env vars", + protocol: "${protocol}", + starttls: "${starttls}", + verifycert: "${verifycert}", + sni: "${sni}", + host: "${host}", + port: "${port}", + binddn: "${binddn}", + pwdmode: "${pwdmode}", + bindpwd: "${bindpwd}", + basedn: "${basedn}", + limit: "${sizelimit}", + paged: "${paged}", + connectTimeout: "${connectTimeout}", + timeout: "${timeout}", + bookmarks: [], + }); // Set environment variables. process.env = { protocol: "ldap", @@ -55,6 +57,7 @@ suite('Extension test suite', () => { host: "myserver.com", port: "1389", binddn: "cn=admin,dc=example,dc=org", + pwdmode: "settings", bindpwd: "foobar", basedn: "dc=example,dc=org", sizelimit: "0", @@ -71,6 +74,7 @@ suite('Extension test suite', () => { assert.strictEqual("myserver.com", connection.getHost(true)); assert.strictEqual("1389", connection.getPort(true)); assert.strictEqual("cn=admin,dc=example,dc=org", connection.getBindDn(true)); + assert.strictEqual("settings", connection.getPwdMode(true)); assert.strictEqual("foobar", connection.getBindPwd(true)); assert.strictEqual("dc=example,dc=org", connection.getBaseDn(true)); assert.strictEqual("0", connection.getLimit(true)); @@ -80,30 +84,30 @@ suite('Extension test suite', () => { }); test('Test binaryGUIDToTextUUID', () => { - assert.strictEqual("{7f613d2a-b1ed-469b-9252-a804d4310c88}", + assert.strictEqual("{7f613d2a-b1ed-469b-9252-a804d4310c88}", utils.binaryGUIDToTextUUID(Buffer.from("Kj1hf+2xm0aSUqgE1DEMiA==", "base64"))); - assert.strictEqual("{ebaf4bc1-9068-4842-a6e5-8955bff33a6f}", + assert.strictEqual("{ebaf4bc1-9068-4842-a6e5-8955bff33a6f}", utils.binaryGUIDToTextUUID(Buffer.from("wUuv62iQQkim5YlVv/M6bw==", "base64"))); - assert.strictEqual("{32b1434c-cc5f-4ec1-8afa-e14c300a9070}", + assert.strictEqual("{32b1434c-cc5f-4ec1-8afa-e14c300a9070}", utils.binaryGUIDToTextUUID(Buffer.from("TEOxMl/MwU6K+uFMMAqQcA==", "base64"))); - assert.strictEqual("{541be56a-668c-4e78-a317-7fb201d5abc0}", + assert.strictEqual("{541be56a-668c-4e78-a317-7fb201d5abc0}", utils.binaryGUIDToTextUUID(Buffer.from("auUbVIxmeE6jF3+yAdWrwA==", "base64"))); }); test('Test binarySIDToText', () => { - assert.strictEqual("S-1-5-21-4169144328-3425172002-2123581430-1103", + assert.strictEqual("S-1-5-21-4169144328-3425172002-2123581430-1103", utils.binarySIDToText(Buffer.from("AQUAAAAAAAUVAAAACBiA+CL6J8z2R5N+TwQAAA==", "base64"))); - assert.strictEqual("S-1-5-21-4169144328-3425172002-2123581430-1140", + assert.strictEqual("S-1-5-21-4169144328-3425172002-2123581430-1140", utils.binarySIDToText(Buffer.from("AQUAAAAAAAUVAAAACBiA+CL6J8z2R5N+dAQAAA==", "base64"))); - assert.strictEqual("S-1-5-21-4169144328-3425172002-2123581430-1130", + assert.strictEqual("S-1-5-21-4169144328-3425172002-2123581430-1130", utils.binarySIDToText(Buffer.from("AQUAAAAAAAUVAAAACBiA+CL6J8z2R5N+agQAAA==", "base64"))); - assert.strictEqual("S-1-5-21-3316208387-3203859757-1631524618-1117", + assert.strictEqual("S-1-5-21-3316208387-3203859757-1631524618-1117", utils.binarySIDToText(Buffer.from("AQUAAAAAAAUVAAAAA1OpxS0F974KFz9hXQQAAA==", "base64"))); }); diff --git a/src/tree-providers/BookmarkTreeDataProvider.ts b/src/tree-providers/BookmarkTreeDataProvider.ts index 17866f0..7705891 100644 --- a/src/tree-providers/BookmarkTreeDataProvider.ts +++ b/src/tree-providers/BookmarkTreeDataProvider.ts @@ -1,4 +1,4 @@ -import { Event, EventEmitter, ExtensionContext, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, workspace } from 'vscode'; +import { Event, EventEmitter, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { LdapConnectionManager } from '../LdapConnectionManager'; /** @@ -6,10 +6,10 @@ import { LdapConnectionManager } from '../LdapConnectionManager'; */ export class BookmarkTreeDataProvider implements TreeDataProvider { - private context: ExtensionContext; + private connectionManager: LdapConnectionManager; - constructor(context: ExtensionContext) { - this.context = context; + constructor(connectionManager: LdapConnectionManager) { + this.connectionManager = connectionManager; } /** @@ -43,7 +43,7 @@ export class BookmarkTreeDataProvider implements TreeDataProvider { } // Get active connection. - const connection = LdapConnectionManager.getActiveConnection(this.context); + const connection = this.connectionManager.getActiveConnection(); if (connection === undefined) { // No active connection: return empty array. return resolve([]); diff --git a/src/tree-providers/ConnectionTreeDataProvider.ts b/src/tree-providers/ConnectionTreeDataProvider.ts index 606d625..9a6dc6b 100644 --- a/src/tree-providers/ConnectionTreeDataProvider.ts +++ b/src/tree-providers/ConnectionTreeDataProvider.ts @@ -7,10 +7,10 @@ import { LdapConnectionManager } from '../LdapConnectionManager'; */ export class ConnectionTreeDataProvider implements TreeDataProvider { - private context: ExtensionContext; + private connectionManager: LdapConnectionManager; - public constructor(context: ExtensionContext) { - this.context = context; + public constructor(connectionManager: LdapConnectionManager) { + this.connectionManager = connectionManager; } /** @@ -22,7 +22,7 @@ export class ConnectionTreeDataProvider implements TreeDataProvider { // Top-level tree items: list of connections. if (!treeItem) { - const connections = LdapConnectionManager.getConnections(); + const connections = this.connectionManager.getConnections(); return Promise.resolve(connections); } diff --git a/src/tree-providers/EntryTreeDataProvider.ts b/src/tree-providers/EntryTreeDataProvider.ts index e22ca63..fe7468e 100644 --- a/src/tree-providers/EntryTreeDataProvider.ts +++ b/src/tree-providers/EntryTreeDataProvider.ts @@ -8,9 +8,11 @@ import { LdapConnectionManager } from '../LdapConnectionManager'; export class EntryTreeDataProvider implements TreeDataProvider { private context: ExtensionContext; + private connectionManager: LdapConnectionManager; - constructor(context: ExtensionContext) { + constructor(context: ExtensionContext, connectionManager: LdapConnectionManager) { this.context = context; + this.connectionManager = connectionManager; } /** @@ -73,7 +75,7 @@ export class EntryTreeDataProvider implements TreeDataProvider { public getChildren(dn?: string): Thenable { return new Promise((resolve, reject) => { // Get active connection. - const connection = LdapConnectionManager.getActiveConnection(this.context); + const connection = this.connectionManager.getActiveConnection(); if (connection === undefined) { // No active connection: return empty array. return resolve([]); @@ -88,7 +90,14 @@ export class EntryTreeDataProvider implements TreeDataProvider { // A parent item was passed i.e. we are not at the top level of the tree. // Send a search request to the LDAP server to fetch the children. // The LDAP search scope is set to "one" so we only get the immediate subordinates https://ldapwiki.com/wiki/SingleLevel - connection.search({ scope: "one", paged: connection.getPagedBool(true) }, dn).then( + connection.search({ + context: this.context, + searchOptions: { + scope: "one", + paged: connection.getPagedBool(true), + }, + base: dn, + }).then( (entries: SearchEntry[]) => { return resolve(entries.map(entry => entry.dn)); }, diff --git a/src/webviews/SearchWebviewViewProvider.ts b/src/webviews/SearchWebviewViewProvider.ts index 72ea845..d7cdc6d 100644 --- a/src/webviews/SearchWebviewViewProvider.ts +++ b/src/webviews/SearchWebviewViewProvider.ts @@ -8,10 +8,12 @@ import { getUri, getWebviewUiToolkitUri } from './utils'; */ export class SearchWebviewViewProvider implements WebviewViewProvider { - private extensionContext: ExtensionContext; + private context: ExtensionContext; + private connectionManager: LdapConnectionManager; - constructor(extensionContext: ExtensionContext) { - this.extensionContext = extensionContext; + constructor(context: ExtensionContext, connectionManager: LdapConnectionManager) { + this.context = context; + this.connectionManager = connectionManager; } /** @@ -19,10 +21,10 @@ export class SearchWebviewViewProvider implements WebviewViewProvider { */ public resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext, token: CancellationToken): void | Thenable { // JS required for the Webview UI toolkit https://github.com/microsoft/vscode-webview-ui-toolkit - const toolkitUri = getWebviewUiToolkitUri(webviewView.webview, this.extensionContext.extensionUri); + const toolkitUri = getWebviewUiToolkitUri(webviewView.webview, this.context.extensionUri); // JS of the webview. - const scriptUri = getUri(webviewView.webview, this.extensionContext.extensionUri, ["assets", "js", "SearchWebviewViewProvider.js"]); + const scriptUri = getUri(webviewView.webview, this.context.extensionUri, ["assets", "js", "SearchWebviewViewProvider.js"]); // Allow JS in the webview. webviewView.webview.options = { @@ -60,7 +62,7 @@ export class SearchWebviewViewProvider implements WebviewViewProvider { switch (message.command) { case 'search': // Get active connection. - const connection = LdapConnectionManager.getActiveConnection(this.extensionContext); + const connection = this.connectionManager.getActiveConnection(); if (connection === undefined) { window.showErrorMessage(`No active connection`); return; @@ -78,12 +80,12 @@ export class SearchWebviewViewProvider implements WebviewViewProvider { const attributes = (message.attributes === '') ? undefined : message.attributes.split(/\r?\n/); // Show search results in a webview. - createSearchResultsWebview(this.extensionContext, connection, message.filter, attributes); + createSearchResultsWebview(this.context, connection, message.filter, attributes); break; } }, undefined, - this.extensionContext.subscriptions + this.context.subscriptions ); } diff --git a/src/webviews/createAddEditConnectionWebview.ts b/src/webviews/createAddEditConnectionWebview.ts index d7fed0f..3b75d26 100644 --- a/src/webviews/createAddEditConnectionWebview.ts +++ b/src/webviews/createAddEditConnectionWebview.ts @@ -1,7 +1,8 @@ import { commands, ExtensionContext, ViewColumn, window } from 'vscode'; import { LdapConnection } from '../LdapConnection'; import { LdapConnectionManager } from '../LdapConnectionManager'; -import { getUri, getWebviewUiToolkitUri } from './utils'; +import { getCodiconsUri, getUri, getWebviewUiToolkitUri } from './utils'; +import { PasswordMode } from '../PasswordMode'; /** * Create a webview to edit or create a connection. @@ -9,7 +10,7 @@ import { getUri, getWebviewUiToolkitUri } from './utils'; * If no connection is provided in the arguments then the form will create a new connection. * Otherwise it will edit the connection. */ -export function createAddEditConnectionWebview(context: ExtensionContext, existingConnection?: LdapConnection) { +export function createAddEditConnectionWebview(context: ExtensionContext, connectionManager: LdapConnectionManager, existingConnection?: LdapConnection) { // Create webview. const panel = window.createWebviewPanel( @@ -28,6 +29,9 @@ export function createAddEditConnectionWebview(context: ExtensionContext, existi // JS of the webview. const scriptUri = getUri(panel.webview, context.extensionUri, ["assets", "js", "createAddEditConnectionWebview.js"]); + // CSS for codicons. + const codiconsUri = getCodiconsUri(panel.webview, context.extensionUri); + // Populate webview HTML. // The VS Code API seems to provide no way to inspect the configuration schema (in package.json). // So make sure all HTML fields listed in the form below match the contributed @@ -37,8 +41,9 @@ export function createAddEditConnectionWebview(context: ExtensionContext, existi - + +
@@ -73,7 +78,20 @@ export function createAddEditConnectionWebview(context: ExtensionContext, existi Bind DN
- Bind Password +

Bind Password mode

+ + Store encrypted in secret storage + Ask on connect + Store as plain text in settings + +
+
+ + Bind Password +
Base DN * @@ -108,26 +126,27 @@ export function createAddEditConnectionWebview(context: ExtensionContext, existi panel.webview.onDidReceiveMessage( message => { // Build connection object. - const newConnection = new LdapConnection( - message.name, - message.protocol, - message.starttls, - message.verifycert, - message.sni, - message.host, - message.port, - message.binddn, - message.bindpwd, - message.basedn, - message.limit, - message.paged, - message.connectTimeout, - message.timeout, + const newConnection = new LdapConnection({ + name: message.name, + protocol: message.protocol, + starttls: message.starttls, + verifycert: message.verifycert, + sni: message.sni, + host: message.host, + port: message.port, + binddn: message.binddn, + pwdmode: message.pwdmode, + bindpwd: message.bindpwd, + basedn: message.basedn, + limit: message.limit, + paged: message.paged, + connectTimeout: message.connectTimeout, + timeout: message.timeout, // Bookmarks are not editable via the connection add/edit form. - // Maintain pre-existing bookarks when editing a connection, and default + // Maintain pre-existing bookmarks when editing a connection, and default // to empty array when adding a new connection. - (existingConnection === undefined) ? [] : existingConnection.getBookmarks() - ); + bookmarks: (existingConnection === undefined) ? [] : existingConnection.getBookmarks(), + }); switch (message.command) { case 'save': // Verify mandatory fields are not empty. @@ -153,12 +172,12 @@ export function createAddEditConnectionWebview(context: ExtensionContext, existi if (existingConnection === undefined) { // Verify connection name does not already exist. - if (LdapConnectionManager.getConnection(newConnection.getName())) { + if (connectionManager.getConnection(newConnection.getName())) { window.showErrorMessage(`A connection named "${newConnection.getName()} already exists, please pick a different name`); return; } - LdapConnectionManager.addConnection(newConnection).then( + connectionManager.addConnection(newConnection).then( value => { // If the connection was successfully added, then refresh the tree view so it shows up. commands.executeCommand("ldap-explorer.refresh"); @@ -171,7 +190,7 @@ export function createAddEditConnectionWebview(context: ExtensionContext, existi } ); } else { - LdapConnectionManager.editConnection(newConnection, existingConnection.getName()).then( + connectionManager.editConnection(newConnection, existingConnection.getName()).then( value => { // If the connection was successfully updated, then refresh the tree view. commands.executeCommand("ldap-explorer.refresh"); @@ -192,7 +211,16 @@ export function createAddEditConnectionWebview(context: ExtensionContext, existi case 'test': // Test connection. - newConnection.search({}).then( + // If the connection is configured to load the password from secret + // storage, then load it from the form / local connection object instead + // ("settings") as it has not yet been persisted in secret storage. + newConnection.search({ + context: context, + searchOptions: {}, + pwdmode: (newConnection.getPwdMode(true) === PasswordMode.secretStorage) + ? PasswordMode.settings + : newConnection.getPwdMode(true) + }).then( value => { window.showInformationMessage('Connection succeeded'); }, diff --git a/src/webviews/createSearchResultsWebview.ts b/src/webviews/createSearchResultsWebview.ts index a6b4720..2eedfd0 100644 --- a/src/webviews/createSearchResultsWebview.ts +++ b/src/webviews/createSearchResultsWebview.ts @@ -69,10 +69,10 @@ export function createSearchResultsWebview(context: ExtensionContext, connection } // Execute ldap search and populate grid as results are received. - connection.search( - getSearchOptions(), - connection.getBaseDn(true), - (entry) => { + connection.search({ + context: context, + searchOptions: getSearchOptions(), + onSearchEntryFound: (entry) => { // Turn LDAP entry into an object that matches the format expected by the grid. // The LDAP attribute name will show up in the grid headers and the values will show up in the cells. // See https://github.com/microsoft/vscode-webview-ui-toolkit/blob/main/src/data-grid/README.md @@ -88,7 +88,7 @@ export function createSearchResultsWebview(context: ExtensionContext, connection row: row, }); } - ).then( + }).then( entries => { // Do nothing: onSearchResultFound callback is provided i.e. results are // displayed as they are received. @@ -129,10 +129,10 @@ export function createSearchResultsWebview(context: ExtensionContext, connection window.showErrorMessage(`Unable to write to ${uriCSV.fsPath}: ${err}`); } // Execute LDAP search. - connection.search( - getSearchOptions(), - connection.getBaseDn(true), - (entry) => { + connection.search({ + context: context, + searchOptions: getSearchOptions(), + onSearchEntryFound: (entry) => { // For each result, format a CSV line and write it to the file. let entryValues: (string | string[])[] = []; attributesToExport.forEach(attributeToExport => { @@ -146,7 +146,7 @@ export function createSearchResultsWebview(context: ExtensionContext, connection } }); } - ).then( + }).then( entries => { // Tell user the export is complete. // Show a button "Open" so the user can immediately read the contents of the CSV. diff --git a/src/webviews/createShowAttributesWebview.ts b/src/webviews/createShowAttributesWebview.ts index 10dbef1..fa1a796 100644 --- a/src/webviews/createShowAttributesWebview.ts +++ b/src/webviews/createShowAttributesWebview.ts @@ -11,7 +11,13 @@ import { sep } from "path"; export function createShowAttributesWebview(connection: LdapConnection, dn: string, context: ExtensionContext) { // Scope is set to "base" so we only get attributes about the entry provided https://ldapwiki.com/wiki/BaseObject - connection.search({ scope: "base" }, dn).then( + connection.search({ + context: context, + searchOptions: { + scope: "base", + }, + base: dn, + }).then( entries => { // Create webview. const panel = window.createWebviewPanel( diff --git a/src/webviews/utils.ts b/src/webviews/utils.ts index e59cacf..b0800b0 100644 --- a/src/webviews/utils.ts +++ b/src/webviews/utils.ts @@ -123,6 +123,24 @@ export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); } +/** + * Utility function to get the URI of the Codicons CSS. + * + * @see https://github.com/microsoft/vscode-extension-samples/tree/main/webview-codicons-sample + */ +export function getCodiconsUri(webview: Webview, extensionUri: Uri) { + // This CSS must be included in the VSIX and as a result is listed as an + // exception in .vscodeignore. + const pathList: string[] = [ + "node_modules", + "@vscode", + "codicons", + "dist", + "codicon.css", + ]; + return getUri(webview, extensionUri, pathList); +} + /** * Utility function to get the URI of the Webview UI toolkit. * diff --git a/tsconfig.json b/tsconfig.json index 05b4c0e..a5e22c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,6 @@ ], "sourceMap": true, "rootDir": "src", - "strict": true, - "noImplicitReturns": true + "strict": true } }