Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand All @@ -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:
Expand Down
28 changes: 27 additions & 1 deletion assets/js/createAddEditConnectionWebview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";
Expand All @@ -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);
6 changes: 6 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
135 changes: 119 additions & 16 deletions src/LdapConnection.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -21,15 +22,33 @@ 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;
private connectTimeout: string;
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,
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<SearchEntry[]> {
pwdmode?: string,
base?: string,
onSearchEntryFound?: (entry: SearchEntry) => void,
}): Thenable<SearchEntry[]> {
return new Promise((resolve, reject) => {
// Get ldapjs client.
const client: Client = createClient({
Expand All @@ -249,37 +293,96 @@ 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<string | undefined> {
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<SearchEntry[]>) => 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}`);
}

// 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);
}
Expand Down
Loading