mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-11 05:43:26 +00:00
Compare commits
25 Commits
v2024.10.0
...
ac/pm-1528
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
599b6e6058 | ||
|
|
bd5bcbebd9 | ||
|
|
bb9ece6078 | ||
|
|
40de47e6e3 | ||
|
|
094ed57e03 | ||
|
|
96a38e2d76 | ||
|
|
9e200c8705 | ||
|
|
ca945318ed | ||
|
|
04abed9251 | ||
|
|
9b08ca6db8 | ||
|
|
0cbe6e9d33 | ||
|
|
dda6dd99ed | ||
|
|
5492466276 | ||
|
|
ef571ec0c3 | ||
|
|
f2bea1b6d7 | ||
|
|
07a1ae6dea | ||
|
|
f23997dd72 | ||
|
|
18547d6eaa | ||
|
|
c3a4f25160 | ||
|
|
e57a52e483 | ||
|
|
ff1380ee67 | ||
|
|
2269b82e7e | ||
|
|
8ab3516377 | ||
|
|
91dfd7e0b7 | ||
|
|
6db28408e6 |
110
.github/workflows/version-bump.yml
vendored
110
.github/workflows/version-bump.yml
vendored
@@ -7,17 +7,11 @@ on:
|
|||||||
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
|
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
enable_slack_notification:
|
|
||||||
description: "Enable Slack notifications for upcoming release?"
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump_version:
|
bump_version:
|
||||||
name: Bump Version
|
name: Bump Version
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
outputs:
|
|
||||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Validate version input
|
- name: Validate version input
|
||||||
if: ${{ inputs.version_number_override != '' }}
|
if: ${{ inputs.version_number_override != '' }}
|
||||||
@@ -25,49 +19,22 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number_override }}
|
version: ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
- name: Slack Notification Check
|
- name: Generate GH App token
|
||||||
run: |
|
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||||
if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then
|
id: app-token
|
||||||
echo "Slack notifications enabled."
|
with:
|
||||||
else
|
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||||
echo "Slack notifications disabled."
|
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||||
|
|
||||||
- name: Login to Azure - CI Subscription
|
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
|
||||||
id: retrieve-secrets
|
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
|
||||||
with:
|
|
||||||
keyvault: "bitwarden-ci"
|
|
||||||
secrets: "github-gpg-private-key,
|
|
||||||
github-gpg-private-key-passphrase"
|
|
||||||
|
|
||||||
- name: Import GPG key
|
|
||||||
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
|
||||||
with:
|
|
||||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
|
||||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
|
||||||
git_user_signingkey: true
|
|
||||||
git_commit_gpgsign: true
|
|
||||||
|
|
||||||
- name: Setup git
|
- name: Setup git
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
git config user.name github-actions
|
||||||
git config --local user.name "bitwarden-devops-bot"
|
git config user.email github-actions@github.com
|
||||||
|
|
||||||
- name: Create Version Branch
|
|
||||||
id: create-branch
|
|
||||||
run: |
|
|
||||||
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
|
|
||||||
git switch -c $NAME
|
|
||||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Get current version
|
- name: Get current version
|
||||||
id: current-version
|
id: current-version
|
||||||
@@ -144,61 +111,4 @@ jobs:
|
|||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
env:
|
run: git push
|
||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
|
||||||
run: git push -u origin $PR_BRANCH
|
|
||||||
|
|
||||||
- name: Generate GH App token
|
|
||||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
|
||||||
id: app-token
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
|
||||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
|
||||||
owner: ${{ github.repository_owner }}
|
|
||||||
|
|
||||||
- name: Create Version PR
|
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
|
||||||
id: create-pr
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
|
||||||
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
|
|
||||||
run: |
|
|
||||||
PR_URL=$(gh pr create --title "$TITLE" \
|
|
||||||
--base "main" \
|
|
||||||
--head "$PR_BRANCH" \
|
|
||||||
--label "version update" \
|
|
||||||
--label "automated pr" \
|
|
||||||
--body "
|
|
||||||
## Type of change
|
|
||||||
- [ ] Bug fix
|
|
||||||
- [ ] New feature development
|
|
||||||
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
|
||||||
- [ ] Build/deploy pipeline (DevOps)
|
|
||||||
- [X] Other
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
|
|
||||||
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Approve PR
|
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
|
||||||
run: gh pr review $PR_NUMBER --approve
|
|
||||||
|
|
||||||
- name: Merge PR
|
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
|
||||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
|
||||||
|
|
||||||
- name: Report upcoming release version to Slack
|
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && inputs.enable_slack_notification == true }}
|
|
||||||
uses: bitwarden/gh-actions/report-upcoming-release-version@main
|
|
||||||
with:
|
|
||||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
|
||||||
project: ${{ github.repository }}
|
|
||||||
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
# Bitwarden Directory Connector
|
# Bitwarden Directory Connector
|
||||||
|
|
||||||
The Bitwarden Directory Connector is a a desktop application used to sync your Bitwarden enterprise organization to an existing directory of users and groups.
|
The Bitwarden Directory Connector is a desktop application used to sync your Bitwarden enterprise organization to an existing directory of users and groups.
|
||||||
|
|
||||||
Supported directories:
|
Supported directories:
|
||||||
|
|
||||||
|
|||||||
7616
package-lock.json
generated
7616
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
73
package.json
73
package.json
@@ -73,84 +73,83 @@
|
|||||||
"test:types": "npx tsc --noEmit"
|
"test:types": "npx tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "17.3.10",
|
"@angular-devkit/build-angular": "19.0.3",
|
||||||
"@angular-eslint/eslint-plugin-template": "17.5.3",
|
"@angular-eslint/eslint-plugin-template": "17.5.3",
|
||||||
"@angular-eslint/template-parser": "17.5.3",
|
"@angular-eslint/template-parser": "17.5.3",
|
||||||
"@angular/compiler-cli": "17.3.12",
|
"@angular/compiler-cli": "19.0.3",
|
||||||
"@electron/notarize": "2.2.1",
|
"@electron/notarize": "2.5.0",
|
||||||
"@electron/rebuild": "3.6.0",
|
"@electron/rebuild": "3.7.1",
|
||||||
"@fluffy-spoon/substitute": "1.208.0",
|
"@fluffy-spoon/substitute": "1.208.0",
|
||||||
"@microsoft/microsoft-graph-types": "2.40.0",
|
"@microsoft/microsoft-graph-types": "2.40.0",
|
||||||
"@ngtools/webpack": "17.3.10",
|
"@ngtools/webpack": "19.0.0",
|
||||||
"@types/inquirer": "8.2.10",
|
"@types/inquirer": "8.2.10",
|
||||||
"@types/jest": "29.5.13",
|
"@types/jest": "29.5.14",
|
||||||
"@types/ldapjs": "2.2.5",
|
|
||||||
"@types/lowdb": "1.0.15",
|
"@types/lowdb": "1.0.15",
|
||||||
"@types/node": "20.16.10",
|
"@types/node": "20.17.9",
|
||||||
"@types/node-fetch": "2.6.11",
|
"@types/node-fetch": "2.6.12",
|
||||||
"@types/node-forge": "1.3.11",
|
"@types/node-forge": "1.3.11",
|
||||||
"@types/proper-lockfile": "4.1.4",
|
"@types/proper-lockfile": "4.1.4",
|
||||||
"@types/tldjs": "2.3.4",
|
"@types/tldjs": "2.3.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||||
"@typescript-eslint/parser": "5.62.0",
|
"@typescript-eslint/parser": "5.62.0",
|
||||||
"clean-webpack-plugin": "4.0.0",
|
"clean-webpack-plugin": "4.0.0",
|
||||||
"concurrently": "9.0.1",
|
"concurrently": "9.1.0",
|
||||||
"copy-webpack-plugin": "12.0.2",
|
"copy-webpack-plugin": "12.0.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"dotenv": "16.4.5",
|
"dotenv": "16.4.5",
|
||||||
"electron": "28.3.3",
|
"electron": "28.3.3",
|
||||||
"electron-builder": "24.13.3",
|
"electron-builder": "24.13.3",
|
||||||
"electron-log": "5.2.0",
|
"electron-log": "5.2.4",
|
||||||
"electron-reload": "2.0.0-alpha.1",
|
"electron-reload": "2.0.0-alpha.1",
|
||||||
"electron-store": "8.2.0",
|
"electron-store": "8.2.0",
|
||||||
"electron-updater": "6.3.4",
|
"electron-updater": "6.3.9",
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-import-resolver-typescript": "3.6.3",
|
"eslint-import-resolver-typescript": "3.6.3",
|
||||||
"eslint-plugin-import": "2.30.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-rxjs": "5.0.3",
|
"eslint-plugin-rxjs": "5.0.3",
|
||||||
"eslint-plugin-rxjs-angular": "2.0.1",
|
"eslint-plugin-rxjs-angular": "2.0.1",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.1",
|
||||||
"html-loader": "5.1.0",
|
"html-loader": "5.1.0",
|
||||||
"html-webpack-plugin": "5.6.0",
|
"html-webpack-plugin": "5.6.3",
|
||||||
"husky": "9.1.6",
|
"husky": "9.1.7",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-junit": "16.0.0",
|
"jest-junit": "16.0.0",
|
||||||
"jest-mock-extended": "3.0.7",
|
"jest-mock-extended": "3.0.7",
|
||||||
"jest-preset-angular": "14.2.4",
|
"jest-preset-angular": "14.4.2",
|
||||||
"lint-staged": "15.2.10",
|
"lint-staged": "15.2.10",
|
||||||
"mini-css-extract-plugin": "2.9.1",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.3.1",
|
||||||
"node-loader": "2.0.0",
|
"node-loader": "2.1.0",
|
||||||
"pkg": "5.8.1",
|
"pkg": "5.8.1",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"rimraf": "5.0.10",
|
"rimraf": "5.0.10",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"sass": "1.79.4",
|
"sass": "1.79.4",
|
||||||
"sass-loader": "16.0.2",
|
"sass-loader": "16.0.4",
|
||||||
"ts-jest": "29.2.5",
|
"ts-jest": "29.2.5",
|
||||||
"ts-loader": "9.5.1",
|
"ts-loader": "9.5.1",
|
||||||
"tsconfig-paths-webpack-plugin": "4.1.0",
|
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||||
"type-fest": "4.26.1",
|
"type-fest": "4.30.0",
|
||||||
"typescript": "5.4.5",
|
"typescript": "5.5.4",
|
||||||
"typescript-transform-paths": "3.5.1",
|
"typescript-transform-paths": "3.5.2",
|
||||||
"webpack": "5.95.0",
|
"webpack": "5.95.0",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-merge": "6.0.1",
|
"webpack-merge": "6.0.1",
|
||||||
"zone.js": "0.14.10",
|
"zone.js": "0.15.0",
|
||||||
"webpack-node-externals": "3.0.0"
|
"webpack-node-externals": "3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "17.3.12",
|
"@angular/animations": "19.0.3",
|
||||||
"@angular/cdk": "17.3.10",
|
"@angular/cdk": "19.0.2",
|
||||||
"@angular/common": "17.3.12",
|
"@angular/common": "19.0.3",
|
||||||
"@angular/compiler": "17.3.12",
|
"@angular/compiler": "19.0.3",
|
||||||
"@angular/core": "17.3.12",
|
"@angular/core": "19.0.3",
|
||||||
"@angular/forms": "17.3.12",
|
"@angular/forms": "19.0.3",
|
||||||
"@angular/platform-browser": "17.3.12",
|
"@angular/platform-browser": "19.0.3",
|
||||||
"@angular/platform-browser-dynamic": "17.3.12",
|
"@angular/platform-browser-dynamic": "19.0.3",
|
||||||
"@angular/router": "17.3.12",
|
"@angular/router": "19.0.3",
|
||||||
"@microsoft/microsoft-graph-client": "3.0.7",
|
"@microsoft/microsoft-graph-client": "3.0.7",
|
||||||
"big-integer": "1.6.52",
|
"big-integer": "1.6.52",
|
||||||
"bootstrap": "5.3.3",
|
"bootstrap": "5.3.3",
|
||||||
@@ -158,20 +157,20 @@
|
|||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"commander": "12.1.0",
|
"commander": "12.1.0",
|
||||||
"core-js": "3.38.1",
|
"core-js": "3.38.1",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.1",
|
||||||
"google-auth-library": "7.14.1",
|
"google-auth-library": "7.14.1",
|
||||||
"googleapis": "73.0.0",
|
"googleapis": "73.0.0",
|
||||||
"https-proxy-agent": "7.0.5",
|
"https-proxy-agent": "7.0.5",
|
||||||
"inquirer": "8.2.6",
|
"inquirer": "8.2.6",
|
||||||
"keytar": "7.9.0",
|
"keytar": "7.9.0",
|
||||||
"ldapjs": "2.3.3",
|
"ldapts": "7.2.1",
|
||||||
"lowdb": "1.0.0",
|
"lowdb": "1.0.0",
|
||||||
"ngx-toastr": "17.0.2",
|
"ngx-toastr": "17.0.2",
|
||||||
"node-fetch": "2.7.0",
|
"node-fetch": "2.7.0",
|
||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"tldjs": "2.3.1",
|
"tldjs": "2.3.1",
|
||||||
"zone.js": "0.14.10"
|
"zone.js": "0.15.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "~20.18.0",
|
"node": "~20.18.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { checkServerIdentity, PeerCertificate } from "tls";
|
import * as tls from "tls";
|
||||||
|
|
||||||
import * as ldap from "ldapjs";
|
import * as ldapts from "ldapts";
|
||||||
|
|
||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
@@ -19,7 +19,7 @@ import { IDirectoryService } from "./directory.service";
|
|||||||
const UserControlAccountDisabled = 2;
|
const UserControlAccountDisabled = 2;
|
||||||
|
|
||||||
export class LdapDirectoryService implements IDirectoryService {
|
export class LdapDirectoryService implements IDirectoryService {
|
||||||
private client: ldap.Client;
|
private client: ldapts.Client;
|
||||||
private dirConfig: LdapConfiguration;
|
private dirConfig: LdapConfiguration;
|
||||||
private syncConfig: SyncConfiguration;
|
private syncConfig: SyncConfiguration;
|
||||||
|
|
||||||
@@ -48,21 +48,25 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
await this.bind();
|
await this.bind();
|
||||||
|
|
||||||
let users: UserEntry[];
|
let users: UserEntry[];
|
||||||
if (this.syncConfig.users) {
|
|
||||||
users = await this.getUsers(force, test);
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups: GroupEntry[];
|
let groups: GroupEntry[];
|
||||||
if (this.syncConfig.groups) {
|
|
||||||
let groupForce = force;
|
try {
|
||||||
if (!groupForce && users != null) {
|
if (this.syncConfig.users) {
|
||||||
const activeUsers = users.filter((u) => !u.deleted && !u.disabled);
|
users = await this.getUsers(force, test);
|
||||||
groupForce = activeUsers.length > 0;
|
|
||||||
}
|
}
|
||||||
groups = await this.getGroups(groupForce);
|
|
||||||
|
if (this.syncConfig.groups) {
|
||||||
|
let groupForce = force;
|
||||||
|
if (!groupForce && users != null) {
|
||||||
|
const activeUsers = users.filter((u) => !u.deleted && !u.disabled);
|
||||||
|
groupForce = activeUsers.length > 0;
|
||||||
|
}
|
||||||
|
groups = await this.getGroups(groupForce);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await this.client.unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.unbind();
|
|
||||||
return [groups, users];
|
return [groups, users];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +105,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
const deletedPath = this.makeSearchPath("CN=Deleted Objects");
|
const deletedPath = this.makeSearchPath("CN=Deleted Objects");
|
||||||
this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter);
|
this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter);
|
||||||
|
|
||||||
const delControl = new (ldap as any).Control({
|
const delControl = new ldapts.Control("1.2.840.113556.1.4.417", { critical: true });
|
||||||
type: "1.2.840.113556.1.4.417",
|
|
||||||
criticality: true,
|
|
||||||
});
|
|
||||||
const deletedUsers = await this.search<UserEntry>(
|
const deletedUsers = await this.search<UserEntry>(
|
||||||
deletedPath,
|
deletedPath,
|
||||||
deletedFilter,
|
deletedFilter,
|
||||||
@@ -120,7 +121,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
|
|
||||||
private buildUser(searchEntry: any, deleted: boolean): UserEntry {
|
private buildUser(searchEntry: any, deleted: boolean): UserEntry {
|
||||||
const user = new UserEntry();
|
const user = new UserEntry();
|
||||||
user.referenceId = searchEntry.objectName;
|
user.referenceId = this.getReferenceId(searchEntry);
|
||||||
user.deleted = deleted;
|
user.deleted = deleted;
|
||||||
|
|
||||||
if (user.referenceId == null) {
|
if (user.referenceId == null) {
|
||||||
@@ -172,7 +173,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
let groupSearchEntries: any[] = [];
|
let groupSearchEntries: any[] = [];
|
||||||
const initialSearchGroupIds = await this.search<string>(path, filter, (se: any) => {
|
const initialSearchGroupIds = await this.search<string>(path, filter, (se: any) => {
|
||||||
groupSearchEntries.push(se);
|
groupSearchEntries.push(se);
|
||||||
return se.objectName;
|
return this.getReferenceId(se);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (searchSinceRevision && initialSearchGroupIds.length === 0) {
|
if (searchSinceRevision && initialSearchGroupIds.length === 0) {
|
||||||
@@ -188,7 +189,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
const userPath = this.makeSearchPath(this.syncConfig.userPath);
|
const userPath = this.makeSearchPath(this.syncConfig.userPath);
|
||||||
const userIdMap = new Map<string, string>();
|
const userIdMap = new Map<string, string>();
|
||||||
await this.search<string>(userPath, userFilter, (se: any) => {
|
await this.search<string>(userPath, userFilter, (se: any) => {
|
||||||
userIdMap.set(se.objectName, this.getExternalId(se, se.objectName));
|
userIdMap.set(this.getReferenceId(se), this.getExternalId(se, this.getReferenceId(se)));
|
||||||
return se;
|
return se;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,7 +205,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
|
|
||||||
private buildGroup(searchEntry: any, userMap: Map<string, string>) {
|
private buildGroup(searchEntry: any, userMap: Map<string, string>) {
|
||||||
const group = new GroupEntry();
|
const group = new GroupEntry();
|
||||||
group.referenceId = searchEntry.objectName;
|
group.referenceId = this.getReferenceId(searchEntry);
|
||||||
if (group.referenceId == null) {
|
if (group.referenceId == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -220,7 +221,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute);
|
const members = this.getAttrVals<string>(searchEntry, this.syncConfig.memberAttribute);
|
||||||
if (members != null) {
|
if (members != null) {
|
||||||
for (const memDn of members) {
|
for (const memDn of members) {
|
||||||
if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) {
|
if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) {
|
||||||
@@ -234,15 +235,26 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getExternalId(searchEntry: any, referenceId: string) {
|
/**
|
||||||
const attrObj = this.getAttrObj(searchEntry, "objectGUID");
|
* The externalId is the "objectGUID" property if present (a unique identifier used by Active Directory),
|
||||||
if (attrObj != null && attrObj._vals != null && attrObj._vals.length > 0) {
|
* otherwise it falls back to the provided referenceId.
|
||||||
return this.bufToGuid(attrObj._vals[0]);
|
*/
|
||||||
|
private getExternalId(searchEntry: ldapts.Entry, referenceId: string) {
|
||||||
|
const attr = this.getAttr<Buffer>(searchEntry, "objectGUID");
|
||||||
|
if (attr != null) {
|
||||||
|
return this.bufToGuid(attr);
|
||||||
} else {
|
} else {
|
||||||
return referenceId;
|
return referenceId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the object's reference id (dn)
|
||||||
|
*/
|
||||||
|
private getReferenceId(entry: ldapts.Entry): string {
|
||||||
|
return entry.dn;
|
||||||
|
}
|
||||||
|
|
||||||
private buildBaseFilter(objectClass: string, subFilter: string): string {
|
private buildBaseFilter(objectClass: string, subFilter: string): string {
|
||||||
let filter = this.buildObjectClassFilter(objectClass);
|
let filter = this.buildObjectClassFilter(objectClass);
|
||||||
if (subFilter != null && subFilter.trim() !== "") {
|
if (subFilter != null && subFilter.trim() !== "") {
|
||||||
@@ -281,42 +293,48 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAttrObj(searchEntry: any, attr: string): any {
|
/**
|
||||||
if (searchEntry == null || searchEntry.attributes == null) {
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all values for an ldap attribute
|
||||||
|
* @param searchEntry The ldap entry
|
||||||
|
* @param attr An attribute name on the ldap entry
|
||||||
|
* @returns An array containing all values of the attribute, or null if there are no values
|
||||||
|
*/
|
||||||
|
private getAttrVals<T extends string | Buffer>(
|
||||||
|
searchEntry: ldapts.Entry,
|
||||||
|
attr: string,
|
||||||
|
): T[] | null {
|
||||||
|
if (searchEntry == null || searchEntry[attr] == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attrs = searchEntry.attributes.filter((a: any) => a.type === attr);
|
const vals = searchEntry[attr];
|
||||||
if (
|
if (!Array.isArray(vals)) {
|
||||||
attrs == null ||
|
return [vals] as T[];
|
||||||
attrs.length === 0 ||
|
|
||||||
attrs[0].vals == null ||
|
|
||||||
attrs[0].vals.length === 0
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return attrs[0];
|
return vals as T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAttrVals(searchEntry: any, attr: string): string[] {
|
/**
|
||||||
const obj = this.getAttrObj(searchEntry, attr);
|
* Get the first value for an ldap attribute
|
||||||
if (obj == null) {
|
* @param searchEntry The ldap entry
|
||||||
return null;
|
* @param attr An attribute name on the ldap entry
|
||||||
}
|
* @returns The first value of the attribute, or null if there is not at least 1 value
|
||||||
return obj.vals;
|
*/
|
||||||
}
|
private getAttr<T extends string | Buffer>(searchEntry: ldapts.Entry, attr: string): T {
|
||||||
|
|
||||||
private getAttr(searchEntry: any, attr: string): string {
|
|
||||||
const vals = this.getAttrVals(searchEntry, attr);
|
const vals = this.getAttrVals(searchEntry, attr);
|
||||||
if (vals == null) {
|
if (vals == null || vals.length < 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return vals[0];
|
|
||||||
|
return vals[0] as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
private entryDisabled(searchEntry: any): boolean {
|
private entryDisabled(searchEntry: any): boolean {
|
||||||
const c = this.getAttr(searchEntry, "userAccountControl");
|
const c = this.getAttr<string>(searchEntry, "userAccountControl");
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
try {
|
try {
|
||||||
const control = parseInt(c, null);
|
const control = parseInt(c, null);
|
||||||
@@ -333,145 +351,103 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
private async search<T>(
|
private async search<T>(
|
||||||
path: string,
|
path: string,
|
||||||
filter: string,
|
filter: string,
|
||||||
processEntry: (searchEntry: any) => T,
|
processEntry: (searchEntry: ldapts.Entry) => T,
|
||||||
controls: ldap.Control[] = [],
|
controls: ldapts.Control[] = [],
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const options: ldap.SearchOptions = {
|
const options: ldapts.SearchOptions = {
|
||||||
filter: filter,
|
filter: filter,
|
||||||
scope: "sub",
|
scope: "sub",
|
||||||
paged: this.dirConfig.pagedSearch,
|
paged: this.dirConfig.pagedSearch,
|
||||||
};
|
};
|
||||||
const entries: T[] = [];
|
const { searchEntries } = await this.client.search(path, options, controls);
|
||||||
return new Promise<T[]>((resolve, reject) => {
|
return searchEntries.map((e) => processEntry(e)).filter((e) => e != null);
|
||||||
this.client.search(path, options, controls, (err, res) => {
|
|
||||||
if (err != null) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.on("error", (resErr) => {
|
|
||||||
reject(resErr);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on("searchEntry", (entry) => {
|
|
||||||
const e = processEntry(entry);
|
|
||||||
if (e != null) {
|
|
||||||
entries.push(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on("end", (result) => {
|
|
||||||
resolve(entries);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async bind(): Promise<any> {
|
private async bind(): Promise<any> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
|
||||||
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
|
throw new Error(this.i18nService.t("dirConfigIncomplete"));
|
||||||
reject(this.i18nService.t("dirConfigIncomplete"));
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
const protocol = "ldap" + (this.dirConfig.ssl && !this.dirConfig.startTls ? "s" : "");
|
|
||||||
const url = protocol + "://" + this.dirConfig.hostname + ":" + this.dirConfig.port;
|
|
||||||
const options: ldap.ClientOptions = {
|
|
||||||
url: url.trim().toLowerCase(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const tlsOptions: any = {};
|
const protocol = this.dirConfig.ssl && !this.dirConfig.startTls ? "ldaps" : "ldap";
|
||||||
if (this.dirConfig.ssl) {
|
|
||||||
if (this.dirConfig.sslAllowUnauthorized) {
|
|
||||||
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
|
|
||||||
}
|
|
||||||
if (!this.dirConfig.startTls) {
|
|
||||||
if (
|
|
||||||
this.dirConfig.sslCaPath != null &&
|
|
||||||
this.dirConfig.sslCaPath !== "" &&
|
|
||||||
fs.existsSync(this.dirConfig.sslCaPath)
|
|
||||||
) {
|
|
||||||
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.dirConfig.sslCertPath != null &&
|
|
||||||
this.dirConfig.sslCertPath !== "" &&
|
|
||||||
fs.existsSync(this.dirConfig.sslCertPath)
|
|
||||||
) {
|
|
||||||
tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.dirConfig.sslKeyPath != null &&
|
|
||||||
this.dirConfig.sslKeyPath !== "" &&
|
|
||||||
fs.existsSync(this.dirConfig.sslKeyPath)
|
|
||||||
) {
|
|
||||||
tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (
|
|
||||||
this.dirConfig.tlsCaPath != null &&
|
|
||||||
this.dirConfig.tlsCaPath !== "" &&
|
|
||||||
fs.existsSync(this.dirConfig.tlsCaPath)
|
|
||||||
) {
|
|
||||||
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
|
const url = protocol + "://" + this.dirConfig.hostname + ":" + this.dirConfig.port;
|
||||||
options.tlsOptions = tlsOptions;
|
const options: ldapts.ClientOptions = {
|
||||||
|
url: url.trim().toLowerCase(),
|
||||||
|
};
|
||||||
|
|
||||||
this.client = ldap.createClient(options);
|
// If using ldaps, TLS options are given to the client constructor
|
||||||
|
if (protocol === "ldaps") {
|
||||||
|
options.tlsOptions = this.buildTlsOptions();
|
||||||
|
}
|
||||||
|
|
||||||
const user =
|
this.client = new ldapts.Client(options);
|
||||||
this.dirConfig.username == null || this.dirConfig.username.trim() === ""
|
|
||||||
? null
|
|
||||||
: this.dirConfig.username;
|
|
||||||
const pass =
|
|
||||||
this.dirConfig.password == null || this.dirConfig.password.trim() === ""
|
|
||||||
? null
|
|
||||||
: this.dirConfig.password;
|
|
||||||
|
|
||||||
if (user == null || pass == null) {
|
const user =
|
||||||
reject(this.i18nService.t("usernamePasswordNotConfigured"));
|
this.dirConfig.username == null || this.dirConfig.username.trim() === ""
|
||||||
return;
|
? null
|
||||||
}
|
: this.dirConfig.username;
|
||||||
|
const pass =
|
||||||
|
this.dirConfig.password == null || this.dirConfig.password.trim() === ""
|
||||||
|
? null
|
||||||
|
: this.dirConfig.password;
|
||||||
|
|
||||||
if (this.dirConfig.startTls && this.dirConfig.ssl) {
|
if (user == null || pass == null) {
|
||||||
this.client.starttls(options.tlsOptions, undefined, (err, res) => {
|
throw new Error(this.i18nService.t("usernamePasswordNotConfigured"));
|
||||||
if (err != null) {
|
}
|
||||||
reject(err.message);
|
|
||||||
} else {
|
// If using StartTLS, TLS options are given to the StartTLS call
|
||||||
this.client.bind(user, pass, (err2) => {
|
if (this.dirConfig.startTls && this.dirConfig.ssl) {
|
||||||
if (err2 != null) {
|
await this.client.startTLS(this.buildTlsOptions());
|
||||||
reject(err2.message);
|
}
|
||||||
} else {
|
|
||||||
resolve();
|
try {
|
||||||
}
|
await this.client.bind(user, pass);
|
||||||
});
|
} catch {
|
||||||
}
|
await this.client.unbind();
|
||||||
});
|
}
|
||||||
} else {
|
|
||||||
this.client.bind(user, pass, (err) => {
|
|
||||||
if (err != null) {
|
|
||||||
reject(err.message);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async unbind(): Promise<void> {
|
private buildTlsOptions(): tls.ConnectionOptions {
|
||||||
return new Promise((resolve, reject) => {
|
const tlsOptions: tls.ConnectionOptions = {};
|
||||||
this.client.unbind((err) => {
|
|
||||||
if (err != null) {
|
if (this.dirConfig.sslAllowUnauthorized) {
|
||||||
reject(err);
|
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
|
||||||
} else {
|
}
|
||||||
resolve();
|
if (!this.dirConfig.startTls) {
|
||||||
}
|
if (
|
||||||
});
|
this.dirConfig.sslCaPath != null &&
|
||||||
});
|
this.dirConfig.sslCaPath !== "" &&
|
||||||
|
fs.existsSync(this.dirConfig.sslCaPath)
|
||||||
|
) {
|
||||||
|
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.dirConfig.sslCertPath != null &&
|
||||||
|
this.dirConfig.sslCertPath !== "" &&
|
||||||
|
fs.existsSync(this.dirConfig.sslCertPath)
|
||||||
|
) {
|
||||||
|
tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.dirConfig.sslKeyPath != null &&
|
||||||
|
this.dirConfig.sslKeyPath !== "" &&
|
||||||
|
fs.existsSync(this.dirConfig.sslKeyPath)
|
||||||
|
) {
|
||||||
|
tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
this.dirConfig.tlsCaPath != null &&
|
||||||
|
this.dirConfig.tlsCaPath !== "" &&
|
||||||
|
fs.existsSync(this.dirConfig.tlsCaPath)
|
||||||
|
) {
|
||||||
|
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
|
||||||
|
|
||||||
|
return tlsOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bufToGuid(buf: Buffer) {
|
private bufToGuid(buf: Buffer) {
|
||||||
@@ -494,7 +470,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
return guid.toLowerCase();
|
return guid.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkServerIdentityAltNames(host: string, cert: PeerCertificate) {
|
private checkServerIdentityAltNames(host: string, cert: tls.PeerCertificate) {
|
||||||
// Fixes the cert representation when subject is empty and altNames are present
|
// Fixes the cert representation when subject is empty and altNames are present
|
||||||
// Required for node versions < 12.14.1 (which could be used for bwdc cli)
|
// Required for node versions < 12.14.1 (which could be used for bwdc cli)
|
||||||
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
|
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
|
||||||
@@ -510,6 +486,6 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return checkServerIdentity(host, cert);
|
return tls.checkServerIdentity(host, cert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user