1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-05 23:53:21 +00:00

Compare commits

...

71 Commits

Author SHA1 Message Date
github-actions[bot]
b0028bb41a Bump version to 2.10.0 (#252)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(cherry picked from commit 536f48b3c7)
2022-03-16 10:55:08 -06:00
Joseph Flinn
e581e1f6e4 Update hotfix release branch name to hotfix-rc (#247)
(cherry picked from commit ae05183aa3)
2022-03-09 12:53:46 -08:00
Thomas Rittson
dd9e03843a Use saveAccount to scaffold new account (#245) 2022-03-07 07:03:27 +10:00
Oscar Hinton
e38ce53ed5 Add eslint (#243) 2022-03-03 11:09:04 +01:00
Micaiah Martin
0c21bcf847 [BEEEP] - Ignored workflow files from triggering builds (#241) 2022-02-25 09:11:11 -05:00
Micaiah Martin
1c6b94e640 Added dry run logic (#242) 2022-02-25 08:10:49 -06:00
Thomas Rittson
ef1c47ab19 Update jslib (#240) 2022-02-24 09:37:37 -05:00
Chad Scharf
64ff16e895 We're Hiring (#239)
Added link to README.md for Bitwarden Careers page.
2022-02-22 14:03:34 +01:00
Micaiah Martin
89860d6770 Added reusable linting workflow (#238) 2022-02-18 13:28:30 -06:00
Thomas Rittson
91ff43a17f Exclude jslib from prettier hook (#236) 2022-02-17 10:37:14 +10:00
Matt Gibson
0f19ebc928 Enforce Hold label (#234)
* Enforce Hold label

* Linting

Co-authored-by: Micaiah Martin <77340197+mimartin12@users.noreply.github.com>
2022-02-16 08:43:38 -06:00
Addison Beck
b48a1d5856 [lib] Update jslib (#235) 2022-02-15 15:07:15 -05:00
github-actions[bot]
7776009a31 Bumped version to 2.9.10 (#232)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-02-11 14:21:25 -08:00
Matt Gibson
ff816035ce Add rxjs (#231) 2022-02-11 15:44:04 -06:00
Thomas Rittson
fe384b14f0 Update jslib (#230) 2022-02-11 00:31:31 -05:00
Thomas Rittson
adeb84f44e Update jslib (#229) 2022-02-11 15:02:57 +10:00
github-actions[bot]
dc2e17c5db Bumped version to 2.9.9 (#228)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-02-10 08:54:51 -08:00
Vincent Salucci
f3d8b39ac5 [Help] Update links to new pattern (#227)
* [Help] Update links to new pattern

* Update jslib

* Updated bwdc cli link
2022-02-09 09:59:50 -06:00
Oscar Hinton
3be1f2eac6 Client & Version headers (#226) 2022-02-08 15:28:19 +01:00
Thomas Rittson
1146c8f5bf [Tech debt] Refactor authService (#213)
* Add OrganizationLogInStrategy

* Use noop TwoFactorService
2022-02-07 21:38:46 -06:00
Thomas Rittson
910bfb945d Make husky pre-commit hook executable (#223) 2022-02-08 08:30:26 +10:00
Addison Beck
4e886c1c15 [chore] Update jslib (#225) 2022-02-07 12:08:33 -05:00
Robyn MacCallum
a4b85f1e30 Fix group only sync errors for AD (#224) 2022-02-07 10:32:37 -05:00
Addison Beck
7c85c9fddd Update jslib (#222) 2022-02-03 14:47:36 -05:00
Vincent Salucci
68c964acaa [Icons] FF - old font cleanup (#221)
* [Icons] Remove FA

* Webpack renderer correction
2022-02-03 10:31:35 -06:00
Addison Beck
1be64836f4 [chore] Update jslib (#217) 2022-01-31 18:01:24 -05:00
Addison Beck
f2389189a3 [chore] Update jslib (#216) 2022-01-28 10:04:08 -05:00
Robyn MacCallum
bb4be6022b Update ldapjs to include fix for EventEmitter issues (#210) 2022-01-27 14:15:48 -05:00
Oscar Hinton
f85a0c5ea5 Fix webpack using double dots (#215) 2022-01-27 19:37:11 +01:00
Vincent Salucci
5afae04b1d [Icons] Update Font Sheet (#203)
* [Icons] Update font sheet

* Updated toaster icon references

* Prettier Updates

* Added import for variable/map references

* Update jslib

* Adding base class to all icon refs

* Removed unused import

* Removed duplicate import

* Update jslib

* Fixed formatting

* Updated eye/eye-slash icon references

* Update jslib

* Update jslib

* Update jslib
2022-01-27 11:10:25 -06:00
Addison Beck
d1b182d20b [bug] Remove redundant state clean call (#214)
* [bug] Remove redundant state clean call

* [refactor] Remove logout override
2022-01-27 08:12:48 -05:00
Addison Beck
9e3d1caee4 [chore] Update jslib & state services to match (#212)
* [chore] Update jslib & state services to match

* [bug] Save userId when migrating state

This is used to check for authentication, so if not present on boot of the app authenticated users will still have to log in again

* [bug] Save added accounts with userId

Currently we are passing in an account object, resulting in a null key. We should be passing in a userId

* [bug] Ensure configs and settings are not cleared on logout

We need to persist directoryConfigruations on logout so that logging out and back in doesn't require folks to need to reconfig their settings

* Remove unneeded LoginSyncService

* Run prettier

* [style] Remove commented lines

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2022-01-20 16:31:46 -05:00
Daniel James Smith
9a78956b23 Bump jslib to include electron minor bump (#208)
* Bump jslib to include electron minor bump

* Add changes to package-lock.json
2022-01-20 10:03:07 +01:00
Robyn MacCallum
822655b944 Only get users for includeGroup filter (#205)
* Only get users for includeGroup filter

* Additional refactor after feedback

* refactor building of user entries

* Update src/services/azure-directory.service.ts

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* Combine user null checks

* Rename variable

* Put deleted users loop back the way it was

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
2022-01-19 15:05:31 -05:00
Oscar Hinton
6dfbe505d9 Fix scoped package name (#209) 2022-01-18 14:04:13 +01:00
Oscar Hinton
0809c2c104 Rename package to @bitwarden/directory-connector (#207) 2022-01-17 17:26:44 +01:00
Vince Grassia
e30000bd00 Update Version Bump action to latest (#206) 2022-01-14 13:31:57 -05:00
Thomas Rittson
90a7601960 Update jslib and minor fixes for account switching (#204)
* Update jslib

* Update stateMigrationService to use enums

* Remove duplicate subclass method

* Update jslib
2022-01-14 05:59:24 +10:00
Daniel James Smith
8a800c6d33 Add ts files to prettier (#202)
* Add ts files to prettier

* Add scss to prettier

* Add all filetypes to prettier and ignore via .prettierignore

* Add --ignore-unknown to prettier
2022-01-07 14:49:42 +01:00
Robyn MacCallum
d0021c9306 Fix date returning as a string (#201) 2022-01-04 09:32:52 -05:00
Daniel James Smith
97673c84da Update year in copyright (#200) 2022-01-04 15:20:55 +01:00
Robyn MacCallum
771a182235 Bug/refresh token fixes (#199)
* Remove NodeApi class extension and use correct apiService in services module

* lint fixes
2021-12-30 15:31:19 -05:00
Robyn MacCallum
857d725a77 override pushAccounts to not delete account from state (#198) 2021-12-29 16:50:45 -05:00
Daniel James Smith
25b3e0f691 Bump electron dependencies (#197) 2021-12-23 12:49:24 +01:00
Addison Beck
d2ba7631b5 [refactor] Implement StateService (#192)
* [refactor(Account Switching)] Implement StateService

* [bug] Migration service updates

* [bug] Fix organizationId coming in as null

* [bug] Use correct storage location

* [bug] Fix secure storage issues

* [bug] Small fixes

* [bug] lint fixes

* [bug] Undo comment

* [bug] Make method names match super

* update jslib

* Add index signature to keys

* Run prettier

* Start dbus

* Start dbus a different way

* Update build.yml

* Add eval

* Init keyring as well

* Remove eval

* Add eval's back

* Remove unused import

* Remove unnecessary null checks

* Change userId to be entityId instead of clientId

* Remove config service

* lint fixes

* Add clientKeys to account

Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
2021-12-22 15:16:23 -05:00
Micaiah Martin
a893c78c74 Added logic based on input (#196) 2021-12-20 14:53:30 -07:00
Oscar Hinton
5ff041aa7b Add .git-blame-ignore-revs (#195) 2021-12-20 17:39:59 +01:00
Oscar Hinton
096196fcd5 Apply Prettier (#194) 2021-12-20 17:14:18 +01:00
Oscar Hinton
225073aa33 Add Prettier configuration (#193) 2021-12-20 16:38:40 +01:00
Daniel James Smith
f8b26d82d8 Update build.yml (#189)
* Pull jslib

* Remove install of npm 8
npm 8 is included in node v16
2021-12-13 17:18:01 +01:00
Daniel James Smith
6b98a46b94 Bump node to v16 (#187)
* Pull in jslib

* Bump engines required to node 16 and npm 8

* Bump @types/node to 16
The dep on node 14.18 will get cleaned up once we bump electron

* Modify build.yml to build with node 16 and npm 8

* Update requirements in README.md

* Use pkg-fetch 3.2.5 to retrieve node 16.13.0

* Change pkg-fetch version back to 3.2

* Bump keytar to 7.7.0

* Add missing package-lock.json for src-cli

* Bump keytar to 7.7.0 in src/package.json

* Add missing package-lock.json in src/

* Bump pkg to 5.5.1

* Modify download url for keytar

* Replace Mac and Win keytar download urls
2021-12-10 21:07:59 +01:00
Oscar Hinton
13572b94ee Upgrade Angular to v12 (#184)
Co-authored-by: Daniel James Smith <djsmith@web.de>
2021-12-09 22:30:45 +01:00
Vince Grassia
999b790557 Fix typo (#185) 2021-12-08 10:35:44 -05:00
Oscar Hinton
7c93d59a42 Replace toaster library (#183) 2021-12-07 20:43:57 +01:00
Oscar Hinton
9bec2aa2f0 BEEEP: Refactor services DI (#180) 2021-12-06 12:03:12 +01:00
Robyn MacCallum
240e1d5813 Don't flag deleted users as duplicates (#181)
* Don't flag deleted users as duplicates

* Fix nearby linting error

* Apply user filter to deleted users as well

* Revert "Apply user filter to deleted users as well"

This reverts commit 1633ee265f.

* Only throw error if any duplicates are not deleted

* Rename processedUsers to processedActiveUsers

* Update src/services/sync.service.ts

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* Update src/services/sync.service.ts

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* Update src/services/sync.service.ts

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
2021-11-30 22:58:40 -05:00
Micaiah Martin
d82f4d90c1 Added version bump workflow (#182) 2021-11-30 15:53:08 -07:00
Daniel James Smith
abc68e8ef9 Add Azure Identity Authority Selector (#176)
* New AzureSettings to select the Identity Authority

* Add fallback for existing customers

* Throw error if Identity Authority is set to invalid value
2021-11-18 19:02:33 +01:00
Daniel James Smith
660ee538ce Update electron to 14.2.0 (#174)
* Pull in jslib and update electron to 14.2.0

* Fix build errors after pulling jslib

* Remove space from tslint ignore comment
2021-11-09 11:38:18 +01:00
Daniel James Smith
a96144d6dc Remove empty catch blocks and remove allow-empty-catch tslint rule (#170)
* Remove empty catch blocks and remove allow-empty-catch tslint rule

* Update jslib to #513

* Fix build errors after update of jslib

* Add missing params to LoginCommand ctor

* Fix build errors due to missing dependencies

* Add changes to package.json and package-lock.json

* Fixed formatting in tslint.json
2021-11-08 12:47:26 +01:00
Vince Grassia
e43d192007 Change release workflow to only allow releases from rc or hotfix branches (#173) 2021-11-05 12:46:03 -04:00
Joseph Flinn
74a018edb8 Version Bump 2.9.8 (#172) 2021-11-04 07:23:10 -07:00
Joseph Flinn
07d0049183 Fix for Linux cli ELF header issue (#171)
* testing new linux build pipeline

* commenting out the check-for-failures job

* fixing syntax error

* fixing the checksum file

* fixing zip archive

* trying the new testing code

* trying to install libsecret-1-0 with sudo

* fixing typo in package version

* fixing the bash testing

* fixing the pwsh to bash conversion

* adding the macos cli job

* switching the keytar release asset name

* reenabling all jobs for final test

* removing the unneeded checksums from the windows cli build

* fixing windows cli build name

* restricting the windows cli build to only building windows
2021-11-04 07:18:37 -07:00
Vince Grassia
5f5358ea0f Add Blockmap artifacts to workflows (#169) 2021-10-29 11:46:29 -04:00
Vince Grassia
36cc6552bf Add support for latest.yml artifacts (#168) 2021-10-27 16:26:03 -04:00
Vince Grassia
05b5fd2eb4 Rename artifact before upload (#167) 2021-10-27 12:37:17 -04:00
Vince Grassia
95f1e86509 Bump version to 2.9.7 (#166) 2021-10-27 11:02:03 -04:00
Thomas Rittson
378dd06274 Add PR template (#165) 2021-10-27 18:59:03 +10:00
Joseph Flinn
314adeb164 Updating the release constraints (#163)
* Updating the release constraints

* removing the master branch release ci code execution

* updating some verbiage
2021-10-22 08:41:09 -07:00
Vince Grassia
cc4f8c9f8d Add notify constraint (#162) 2021-10-15 13:06:39 -04:00
Vince Grassia
35b0e81beb Add Slack alerts for Build workflow failures (#161) 2021-10-14 14:34:09 -04:00
105 changed files with 16120 additions and 17426 deletions

View File

@@ -7,10 +7,9 @@ root = true
[*]
end_of_line = lf
insert_final_newline = true
quote_type = single
# Set default charset
[*.{js,ts,scss,html}]
charset = utf-8
indent_style = space
indent_size = 4
indent_size = 2

9
.eslintignore Normal file
View File

@@ -0,0 +1,9 @@
dist
build
build-cli
jslib
webpack.cli.js
webpack.main.js
webpack.renderer.js
**/node_modules

32
.eslintrc.json Normal file
View File

@@ -0,0 +1,32 @@
{
"root": true,
"env": {
"browser": true,
"node": true
},
"extends": ["./jslib/shared/eslintrc.json"],
"rules": {
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always",
"pathGroups": [
{
"pattern": "jslib-*/**",
"group": "external",
"position": "after"
},
{
"pattern": "src/**/*",
"group": "parent",
"position": "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"]
}
]
}
}

2
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,2 @@
# Apply Prettier https://github.com/bitwarden/directory-connector/pull/194
096196fcd512944d1c3d9c007647a1319b032639

33
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,33 @@
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
## Objective
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
## Code changes
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
<!--Also refer to any related changes or PRs in other repositories-->
- **file.ext:** Description of what was changed and why
## Screenshots
<!--Required for any UI changes. Delete if not applicable-->
## Testing requirements
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
## Before you submit
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
- [ ] This change requires a **documentation update** (notify the documentation team)
- [ ] This change has particular **deployment requirements** (notify the DevOps team)

View File

@@ -5,6 +5,8 @@ on:
push:
branches-ignore:
- 'l10n_master'
paths-ignore:
- '.github/workflows/**'
jobs:
@@ -40,13 +42,191 @@ jobs:
echo "::set-output name=package_version::$PKG_VERSION"
cli:
name: CLI
linux-cli:
name: Build Linux CLI
runs-on: ubuntu-20.04
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_PKG_FETCH_NODE_VERSION: 16.13.0
_PKG_FETCH_VERSION: 3.2
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '16'
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
- name: Get pkg-fetch
run: |
cd $HOME
fetchedUrl="https://github.com/vercel/pkg-fetch/releases/download/v$_PKG_FETCH_VERSION/node-v$_PKG_FETCH_NODE_VERSION-linux-x64"
mkdir -p .pkg-cache/v$_PKG_FETCH_VERSION
wget $fetchedUrl -O "./.pkg-cache/v$_PKG_FETCH_VERSION/fetched-v$_PKG_FETCH_NODE_VERSION-linux-x64"
- name: Keytar
run: |
keytarVersion=$(cat src/package.json | jq -r '.dependencies.keytar')
keytarTar="keytar-v$keytarVersion-napi-v3-linux-x64.tar"
keytarTarGz="$keytarTar.gz"
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
mkdir -p ./keytar/linux
wget $keytarUrl -O ./keytar/linux/$keytarTarGz
tar -xvf ./keytar/linux/$keytarTarGz -C ./keytar/linux
- name: Install
run: npm install
- name: Package CLI
run: npm run dist:cli:lin
- name: Zip
run: |
zip -j ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip ./dist-cli/linux/bwdc ./keytar/linux/build/Release/keytar.node
- name: Create checksums
run: |
sha256sum ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip | cut -d " " -f 1 > ./dist-cli/bwdc-linux-sha256-$_PACKAGE_VERSION.txt
- name: Version Test
run: |
sudo apt install libsecret-1-0 dbus-x11 gnome-keyring
eval $(dbus-launch --sh-syntax)
eval $(echo -n "" | /usr/bin/gnome-keyring-daemon --login)
eval $(/usr/bin/gnome-keyring-daemon --components=secrets --start)
mkdir -p test/linux
unzip ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip -d ./test/linux
testVersion=$(./test/linux/bwdc -v)
echo "version: $_PACKAGE_VERSION"
echo "testVersion: $testVersion"
if [ "$testVersion" != "$_PACKAGE_VERSION" ]; then
echo "Version test failed."
exit 1
fi
- name: Upload Linux Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Linux checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
macos-cli:
name: Build Mac CLI
runs-on: macos-11
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_PKG_FETCH_NODE_VERSION: 16.13.0
_PKG_FETCH_VERSION: 3.2
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '16'
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
- name: Get pkg-fetch
run: |
cd $HOME
fetchedUrl="https://github.com/vercel/pkg-fetch/releases/download/v$_PKG_FETCH_VERSION/node-v$_PKG_FETCH_NODE_VERSION-macos-x64"
mkdir -p .pkg-cache/v$_PKG_FETCH_VERSION
wget $fetchedUrl -O "./.pkg-cache/v$_PKG_FETCH_VERSION/fetched-v$_PKG_FETCH_NODE_VERSION-macos-x64"
- name: Keytar
run: |
keytarVersion=$(cat src/package.json | jq -r '.dependencies.keytar')
keytarTar="keytar-v$keytarVersion-napi-v3-darwin-x64.tar"
keytarTarGz="$keytarTar.gz"
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
mkdir -p ./keytar/macos
wget $keytarUrl -O ./keytar/macos/$keytarTarGz
tar -xvf ./keytar/macos/$keytarTarGz -C ./keytar/macos
- name: Install
run: npm install
- name: Package CLI
run: npm run dist:cli:mac
- name: Zip
run: |
zip -j ./dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip ./dist-cli/macos/bwdc ./keytar/macos/build/Release/keytar.node
- name: Create checksums
run: |
sha256sum ./dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip | cut -d " " -f 1 > ./dist-cli/bwdc-macos-sha256-$_PACKAGE_VERSION.txt
- name: Version Test
run: |
mkdir -p test/macos
unzip ./dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip -d ./test/macos
testVersion=$(./test/macos/bwdc -v)
echo "version: $_PACKAGE_VERSION"
echo "testVersion: $testVersion"
if [ "$testVersion" != "$_PACKAGE_VERSION" ]; then
echo "Version test failed."
exit 1
fi
- name: Upload Mac Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Mac checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
windows-cli:
name: Build Windows CLI
runs-on: windows-2019
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_WIN_PKG_FETCH_VERSION: 14.17.6
_WIN_PKG_FETCH_VERSION: 16.13.0
_WIN_PKG_VERSION: 3.2
steps:
- name: Checkout repo
@@ -60,21 +240,13 @@ jobs:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
node-version: '16'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
- name: Setting WIN_PKG
run: |
echo "WIN_PKG=$env:WIN_PKG" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "version: $env:pkgVersion"
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v3.0\fetched-v14.16.1-win-x64
- name: Get pkg-fetch
shell: pwsh
run: |
@@ -90,25 +262,16 @@ jobs:
shell: pwsh
run: |
$keytarVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).dependencies.keytar
$nodeModVersion = node -e "console.log(process.config.variables.node_module_version)"
$keytarTar = "keytar-v${keytarVersion}-node-v${nodeModVersion}-{0}-x64.tar"
$keytarTar = "keytar-v${keytarVersion}-napi-v3-{0}-x64.tar"
$keytarTarGz = "${keytarTar}.gz"
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}"
New-Item -ItemType directory -Path ./keytar/macos | Out-Null
New-Item -ItemType directory -Path ./keytar/linux | Out-Null
New-Item -ItemType directory -Path ./keytar/windows | Out-Null
Invoke-RestMethod -Uri $($keytarUrl -f "darwin") -OutFile "./keytar/macos/$($keytarTarGz -f "darwin")"
Invoke-RestMethod -Uri $($keytarUrl -f "linux") -OutFile "./keytar/linux/$($keytarTarGz -f "linux")"
Invoke-RestMethod -Uri $($keytarUrl -f "win32") -OutFile "./keytar/windows/$($keytarTarGz -f "win32")"
7z e "./keytar/macos/$($keytarTarGz -f "darwin")" -o"./keytar/macos"
7z e "./keytar/linux/$($keytarTarGz -f "linux")" -o"./keytar/linux"
7z e "./keytar/windows/$($keytarTarGz -f "win32")" -o"./keytar/windows"
7z e "./keytar/macos/$($keytarTar -f "darwin")" -o"./keytar/macos"
7z e "./keytar/linux/$($keytarTar -f "linux")" -o"./keytar/linux"
7z e "./keytar/windows/$($keytarTar -f "win32")" -o"./keytar/windows"
- name: Setup Version Info
@@ -163,14 +326,12 @@ jobs:
run: npm install
- name: Package CLI
run: npm run dist:cli
run: npm run dist:cli:win
- name: Zip
shell: cmd
run: |
7z a ./dist-cli/bwdc-windows-%_PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node
7z a ./dist-cli/bwdc-macos-%_PACKAGE_VERSION%.zip ./dist-cli/macos/bwdc ./keytar/macos/keytar.node
7z a ./dist-cli/bwdc-linux-%_PACKAGE_VERSION%.zip ./dist-cli/linux/bwdc ./keytar/linux/keytar.node
- name: Version Test
run: |
@@ -186,56 +347,24 @@ jobs:
run: |
checksum -f="./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:_PACKAGE_VERSION}.txt
checksum -f="./dist-cli/bwdc-macos-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-macos-sha256-${env:_PACKAGE_VERSION}.txt
checksum -f="./dist-cli/bwdc-linux-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-linux-sha256-${env:_PACKAGE_VERSION}.txt
- name: Upload Windows Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Mac Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4
with:
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Linux Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4
with:
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Windows checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
- name: Upload Mac checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4
with:
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
- name: Upload Linux checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4
with:
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
windows-gui:
name: Windows GUI
name: Build Windows GUI
runs-on: windows-2019
needs: setup
env:
@@ -249,11 +378,10 @@ jobs:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
node-version: '16'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
@@ -276,8 +404,8 @@ jobs:
- name: Install Node dependencies
run: npm install
- name: Run linter
run: npm run lint
# - name: Run linter
# run: npm run lint
- name: Build & Sign
run: npm run dist:win
@@ -289,26 +417,37 @@ jobs:
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
- name: List Dist
run: dir ./dist
- name: Upload Portable Executable to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable Blockmap to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
if-no-files-found: error
linux:
name: Linux
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: latest.yml
path: ./dist/latest.yml
if-no-files-found: error
linux-gui:
name: Build Linux GUI
runs-on: ubuntu-20.04
needs: setup
env:
@@ -317,11 +456,10 @@ jobs:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
node-version: '16'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
@@ -347,15 +485,22 @@ jobs:
run: npm run dist:lin
- name: Upload AppImage
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
if-no-files-found: error
macos:
name: MacOS
macos-gui:
name: Build MacOS GUI
runs-on: macos-11
needs: setup
env:
@@ -364,11 +509,10 @@ jobs:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
node-version: '16'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
@@ -436,8 +580,8 @@ jobs:
- name: Install Node dependencies
run: npm install
- name: Run linter
run: npm run lint
# - name: Run linter
# run: npm run lint
- name: Build application
run: npm run dist:mac
@@ -445,16 +589,102 @@ jobs:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
- name: Rename Zip Artifact
run: |
cd dist
mv "Bitwarden Directory Connector-${{ env._PACKAGE_VERSION }}-mac.zip" \
"Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip"
- name: Upload .zip artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden Directory Connector-${{ env._PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.4
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
if-no-files-found: error
- name: Upload .dmg Blockmap artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: latest-mac.yml
path: ./dist/latest-mac.yml
if-no-files-found: error
check-failures:
name: Check for failures
runs-on: ubuntu-20.04
needs:
- cloc
- setup
- linux-cli
- macos-cli
- windows-cli
- windows-gui
- linux-gui
- macos-gui
steps:
- name: Check if any job failed
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
env:
CLOC_STATUS: ${{ needs.cloc.result }}
SETUP_STATUS: ${{ needs.setup.result }}
LINUX_CLI_STATUS: ${{ needs.linux-cli.result }}
MACOS_CLI_STATUS: ${{ needs.macos-cli.result }}
WINDOWS_CLI_STATUS: ${{ needs.windows-cli.result }}
WINDOWS_GUI_STATUS: ${{ needs.windows-gui.result }}
LINUX_GUI_STATUS: ${{ needs.linux-gui.result }}
MACOS_GUI_STATUS: ${{ needs.macos-gui.result }}
run: |
if [ "$CLOC_STATUS" = "failure" ]; then
exit 1
elif [ "$SETUP_STATUS" = "failure" ]; then
exit 1
elif [ "$LINUX_CLI_STATUS" = "failure" ]; then
exit 1
elif [ "$MACOS_CLI_STATUS" = "failure" ]; then
exit 1
elif [ "$WINDOWS_CLI_STATUS" = "failure" ]; then
exit 1
elif [ "$WINDOWS_GUI_STATUS" = "failure" ]; then
exit 1
elif [ "$LINUX_GUI_STATUS" = "failure" ]; then
exit 1
elif [ "$MACOS_GUI_STATUS" = "failure" ]; then
exit 1
fi
- name: Login to Azure - Prod Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
if: failure()
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
if: failure()
with:
keyvault: "bitwarden-prod-kv"
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
with:
status: ${{ job.status }}

16
.github/workflows/enforce-labels.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Enforce PR labels
on:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
jobs:
enforce-label:
name: EnforceLabel
runs-on: ubuntu-20.04
steps:
- name: Enforce Label
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
with:
BANNED_LABELS: "hold"
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"

View File

@@ -3,6 +3,16 @@ name: Release
on:
workflow_dispatch:
inputs:
release_type:
description: 'Release Options'
required: true
default: 'Initial Release'
type: choice
options:
- Initial Release
- Redeploy
- Dry Run
jobs:
setup:
@@ -10,18 +20,17 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Branch check
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]]; then
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
echo "==================================="
echo "[!] Can only release from rc branch"
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
echo "==================================="
exit 1
fi
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
with:
ref: rc
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Retrieve Directory Connector release version
id: retrieve-version
@@ -30,6 +39,7 @@ jobs:
echo "::set-output name=package_version::$PKG_VERSION"
- name: Check to make sure Mobile release version has been bumped
if: ${{ github.event.inputs.release_type == 'Initial Release' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
@@ -44,15 +54,22 @@ jobs:
fi
shell: bash
- name: Get branch name
id: branch
run: |
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch-name::$BRANCH_NAME"
- name: Download all artifacts
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
with:
workflow: build.yml
workflow_conclusion: success
branch: rc
branch: ${{ steps.branch.outputs.branch-name }}
- name: Create release
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09 # v2.8.5
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
env:
PKG_VERSION: ${{ steps.retrieve-version.outputs.package_version }}
with:
@@ -64,9 +81,14 @@ jobs:
./bwdc-linux-sha256-${{ env.PKG_VERSION }}.txt,
./Bitwarden-Connector-Portable-${{ env.PKG_VERSION }}.exe,
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe,
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe.blockmap,
./Bitwarden-Connector-${{ env.PKG_VERSION }}-x86_64.AppImage,
./Bitwarden-Connector-${{ env.PKG_VERSION }}-mac.zip,
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg"
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg,
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg.blockmap,
./latest-linux.yml,
./latest-mac.yml,
./latest.yml"
commit: ${{ github.sha }}
tag: v${{ env.PKG_VERSION }}
name: Version ${{ env.PKG_VERSION }}

65
.github/workflows/version-bump.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
---
name: Version Bump
on:
workflow_dispatch:
inputs:
version_number:
description: "New Version"
required: true
jobs:
bump_version:
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
runs-on: ubuntu-20.04
steps:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Create Version Branch
run: |
git switch -c version_bump_${{ github.event.inputs.version_number }}
git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Checkout Version Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
with:
ref: version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - Package
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/package.json"
- name: Commit files
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
- name: Push changes
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Create Version PR
env:
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
BASE_BRANCH: master
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
run: |
gh pr create --title "$TITLE" \
--base "$BASE" \
--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 ${{ github.event.inputs.version_number }}"

11
.github/workflows/workflow-linter.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
---
name: Workflow Linter
on:
pull_request:
paths:
- .github/workflows/**
jobs:
call-workflow:
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@master

1
.husky/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

12
.prettierignore Normal file
View File

@@ -0,0 +1,12 @@
# Build directories
build
build-cli
dist
jslib
# External libraries / auto synced locales
src/locales
# Github Workflows
.github/workflows

3
.prettierrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"printWidth": 100
}

66
.vscode/launch.json vendored
View File

@@ -1,48 +1,40 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Electron: Main",
"protocol": "inspector",
"cwd": "${workspaceRoot}/build",
"runtimeArgs": [
"--remote-debugging-port=9223",
"."
],
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"sourceMaps": true
{
"type": "node",
"request": "launch",
"name": "Electron: Main",
"protocol": "inspector",
"cwd": "${workspaceRoot}/build",
"runtimeArgs": ["--remote-debugging-port=9223", "."],
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
{
"name": "Electron: Renderer",
"type": "chrome",
"request": "attach",
"port": 9223,
"webRoot": "${workspaceFolder}/build",
"sourceMaps": true
"sourceMaps": true
},
{
"type": "node",
"request": "launch",
"name": "Debug CLI",
"protocol": "inspector",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/build-cli/bwdc.js",
"args": [
"sync"
]
"name": "Electron: Renderer",
"type": "chrome",
"request": "attach",
"port": 9223,
"webRoot": "${workspaceFolder}/build",
"sourceMaps": true
},
{
"type": "node",
"request": "launch",
"name": "Debug CLI",
"protocol": "inspector",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/build-cli/bwdc.js",
"args": ["sync"]
}
],
"compounds": [
{
"name": "Electron: All",
"configurations": [
"Electron: Main",
"Electron: Renderer"
]
}
{
"name": "Electron: All",
"configurations": ["Electron: Main", "Electron: Renderer"]
}
]
}

View File

@@ -6,6 +6,7 @@
The Bitwarden Directory Connector is a a desktop application used to sync your Bitwarden enterprise organization to an existing directory of users and groups.
Supported directories:
- Active Directory
- Any other LDAP-based directory
- Azure Active Directory
@@ -14,7 +15,7 @@ Supported directories:
The application is written using Electron with Angular and installs on Windows, macOS, and Linux distributions.
[![Platforms](https://imgur.com/SLv9paA.png "Windows, macOS, and Linux")](https://help.bitwarden.com/article/directory-sync/#download-and-install)
[![Platforms](https://imgur.com/SLv9paA.png "Windows, macOS, and Linux")](https://bitwarden.com/help/directory-sync/#download-and-install)
![Directory Connector](https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/directory-connector-macos.png "Dashboard")
@@ -41,13 +42,13 @@ bwdc config --help
**Detailed Documentation**
We provide detailed documentation and examples for using the Directory Connector CLI in our help center at https://help.bitwarden.com/article/directory-sync/#command-line-interface.
We provide detailed documentation and examples for using the Directory Connector CLI in our help center at https://bitwarden.com/help/directory-sync-cli/.
## Build/Run
**Requirements**
- [Node.js](https://nodejs.org) v14
- [Node.js](https://nodejs.org) v16.13.1 (LTS)
- Windows users: To compile the native node modules used in the app you will need the Visual C++ toolset, available through the standard Visual Studio installer (recommended) or by installing [`windows-build-tools`](https://github.com/felixrieseberg/windows-build-tools) through `npm`. See more at [Compiling native Addon modules](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules).
**Run the app**
@@ -73,8 +74,32 @@ You can then run commands from the `./build-cli` folder:
node ./build-cli/bwdc.js --help
```
## We're Hiring!
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
### Prettier
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
1. Check out your local Branch
2. Run `git merge 225073aa335d33ad905877b68336a9288e89ea10`
3. Resolve any merge conflicts, commit.
4. Run `npm run prettier`
5. Commit
6. Run `git merge -Xours 096196fcd512944d1c3d9c007647a1319b032639`
7. Push
#### Git blame
We also recommend that you configure git to ignore the prettier revision using:
```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

View File

@@ -7,7 +7,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate.
third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder.

2
jslib

Submodule jslib updated: cb00604617...1cbc119ad8

17979
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
{
"name": "bitwarden-directory-connector",
"productName": "Bitwarden Directory Connector",
"name": "@bitwarden/directory-connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "0.0.0",
"keywords": [
@@ -27,8 +26,8 @@
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"rebuild": "electron-rebuild",
"reset": "rimraf ./node_modules/keytar/* && npm install",
"lint": "tslint 'src/**/*.ts' || true",
"lint:fix": "tslint 'src/**/*.ts' --fix",
"lint": "eslint . && prettier --check .",
"lint:fix": "eslint . --fix",
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
"build:main": "webpack --config webpack.main.js",
"build:renderer": "webpack --config webpack.renderer.js",
@@ -59,11 +58,17 @@
"dist:cli:lin": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:lin",
"publish:lin": "npm run build:dist && npm run clean:dist && electron-builder --linux --x64 -p always",
"publish:mac": "npm run build:dist && npm run clean:dist && electron-builder --mac -p always",
"publish:win": "npm run build:dist && npm run clean:dist && electron-builder --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\""
"publish:win": "npm run build:dist && npm run clean:dist && electron-builder --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
"prettier": "prettier --write .",
"prepare": "husky install"
},
"build": {
"extraMetadata": {
"name": "bitwarden-directory-connector"
},
"productName": "Bitwarden Directory Connector",
"appId": "com.bitwarden.directory-connector",
"copyright": "Copyright © 2015-2020 Bitwarden Inc.",
"copyright": "Copyright © 2015-2022 Bitwarden Inc.",
"directories": {
"buildResources": "resources",
"output": "dist",
@@ -132,51 +137,63 @@
}
},
"devDependencies": {
"@angular/compiler-cli": "^11.2.11",
"@angular/compiler-cli": "^12.2.13",
"@microsoft/microsoft-graph-types": "^1.4.0",
"@ngtools/webpack": "^11.2.10",
"@ngtools/webpack": "^12.2.13",
"@types/ldapjs": "^1.0.10",
"@types/node": "^14.14.43",
"@types/node": "^16.11.12",
"@types/proper-lockfile": "^4.1.1",
"clean-webpack-plugin": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^5.12.1",
"@typescript-eslint/parser": "^5.12.1",
"clean-webpack-plugin": "^4.0.0",
"concurrently": "^6.0.2",
"copy-webpack-plugin": "^6.4.0",
"copy-webpack-plugin": "^10.0.0",
"cross-env": "^7.0.3",
"css-loader": "^5.2.4",
"del": "^6.0.0",
"electron-builder": "^22.11.7",
"css-loader": "^6.5.1",
"electron-builder": "^22.14.5",
"electron-notarize": "^1.1.1",
"electron-rebuild": "^3.2.3",
"electron-rebuild": "^3.2.5",
"electron-reload": "^1.5.0",
"file-loader": "^6.2.0",
"font-awesome": "4.7.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.1",
"mini-css-extract-plugin": "^1.5.0",
"node-loader": "^1.0.3",
"pkg": "^5.1.0",
"eslint": "^8.9.0",
"eslint-config-prettier": "^8.4.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.4",
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"husky": "^7.0.4",
"lint-staged": "^12.1.3",
"mini-css-extract-plugin": "^2.4.5",
"node-loader": "^2.0.0",
"pkg": "^5.5.1",
"prebuild-install": "^5.0.0",
"prettier": "^2.5.1",
"rimraf": "^3.0.2",
"sass": "^1.32.11",
"sass-loader": "^10.1.1",
"sass-loader": "^12.4.0",
"tapable": "^1.1.3",
"ts-loader": "^8.1.0",
"ts-loader": "^9.2.5",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"tslint": "~6.1.0",
"tslint-loader": "^3.5.4",
"typescript": "4.1.5",
"webpack": "^4.46.0",
"webpack-cli": "^4.6.0",
"webpack-merge": "^5.7.3",
"webpack-node-externals": "^3.0.0",
"prebuild-install": "^5.0.0"
"typescript": "4.3.5",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1",
"webpack-merge": "^5.8.0",
"webpack-node-externals": "^3.0.0"
},
"dependencies": {
"@angular/animations": "^12.2.13",
"@angular/cdk": "^12.2.13",
"@angular/common": "^12.2.13",
"@angular/compiler": "^12.2.13",
"@angular/core": "^12.2.13",
"@angular/forms": "^12.2.13",
"@angular/platform-browser": "^12.2.13",
"@angular/platform-browser-dynamic": "^12.2.13",
"@angular/router": "^12.2.13",
"@bitwarden/jslib-angular": "file:jslib/angular",
"@bitwarden/jslib-common": "file:jslib/common",
"@bitwarden/jslib-electron": "file:jslib/electron",
"@bitwarden/jslib-node": "file:jslib/node",
"@microsoft/microsoft-graph-client": "^2.2.1",
"angular2-toaster": "^11.0.1",
"bootstrap": "^4.6.0",
"chalk": "^4.1.1",
"commander": "^7.2.0",
@@ -185,13 +202,19 @@
"form-data": "^4.0.0",
"googleapis": "^73.0.0",
"inquirer": "8.0.0",
"ldapjs": "git+https://git@github.com/kspearrin/node-ldapjs.git",
"ldapjs": "2.3.1",
"lunr": "^2.3.9",
"ngx-toastr": "14.1.4",
"open": "^8.0.6",
"proper-lockfile": "^4.1.2"
"proper-lockfile": "^4.1.2",
"rxjs": "^7.4.0"
},
"engines": {
"node": "~14",
"npm": "~7"
"node": "~16",
"npm": "~8"
},
"lint-staged": {
"./!(jslib)**": "prettier --ignore-unknown --write",
"*.ts": "eslint --fix"
}
}

View File

@@ -1,18 +1,19 @@
require('dotenv').config();
const { notarize } = require('electron-notarize');
/* eslint-disable @typescript-eslint/no-var-requires */
require("dotenv").config();
const { notarize } = require("electron-notarize");
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== 'darwin') {
return;
}
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
const appName = context.packager.appInfo.productFilename;
return await notarize({
appBundleId: 'com.bitwarden.directory-connector',
appPath: `${appOutDir}/${appName}.app`,
appleId: appleId,
appleIdPassword: appleIdPassword,
});
const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== "darwin") {
return;
}
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
const appName = context.packager.appInfo.productFilename;
return await notarize({
appBundleId: "com.bitwarden.directory-connector",
appPath: `${appOutDir}/${appName}.app`,
appleId: appleId,
appleIdPassword: appleIdPassword,
});
};

View File

@@ -1,22 +1,20 @@
exports.default = async function(configuration) {
if (
parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 &&
configuration.path.slice(-4) == ".exe"
) {
console.log(`[*] Signing file: ${configuration.path}`)
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
exports.default = async function (configuration) {
if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") {
console.log(`[*] Signing file: ${configuration.path}`);
require("child_process").execSync(
`azuresigntool sign ` +
`-kvu ${process.env.SIGNING_VAULT_URL} ` +
`-kvi ${process.env.SIGNING_CLIENT_ID} ` +
`-kvt ${process.env.SIGNING_TENANT_ID} ` +
`-kvs ${process.env.SIGNING_CLIENT_SECRET} ` +
`-kvc ${process.env.SIGNING_CERT_NAME} ` +
`-fd ${configuration.hash} ` +
`-du ${configuration.site} ` +
`-tr http://timestamp.digicert.com ` +
`"${configuration.path}"`,
`-kvu ${process.env.SIGNING_VAULT_URL} ` +
`-kvi ${process.env.SIGNING_CLIENT_ID} ` +
`-kvt ${process.env.SIGNING_TENANT_ID} ` +
`-kvs ${process.env.SIGNING_CLIENT_SECRET} ` +
`-kvc ${process.env.SIGNING_CERT_NAME} ` +
`-fd ${configuration.hash} ` +
`-du ${configuration.site} ` +
`-tr http://timestamp.digicert.com ` +
`"${configuration.path}"`,
{
stdio: "inherit"
stdio: "inherit",
}
);
}

1006
src-cli/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"name": "bitwarden-directory-connector",
"name": "@bitwarden/directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2.9.5",
@@ -19,6 +19,6 @@
},
"dependencies": {
"browser-hrtime": "^1.1.8",
"keytar": "7.6.0"
"keytar": "^7.7.0"
}
}

View File

@@ -0,0 +1,66 @@
import { StateService as BaseStateServiceAbstraction } from "jslib-common/abstractions/state.service";
import { StorageOptions } from "jslib-common/models/domain/storageOptions";
import { DirectoryType } from "src/enums/directoryType";
import { Account } from "src/models/account";
import { AzureConfiguration } from "src/models/azureConfiguration";
import { GSuiteConfiguration } from "src/models/gsuiteConfiguration";
import { LdapConfiguration } from "src/models/ldapConfiguration";
import { OktaConfiguration } from "src/models/oktaConfiguration";
import { OneLoginConfiguration } from "src/models/oneLoginConfiguration";
import { SyncConfiguration } from "src/models/syncConfiguration";
export abstract class StateService extends BaseStateServiceAbstraction<Account> {
getDirectory: <IConfiguration>(type: DirectoryType) => Promise<IConfiguration>;
setDirectory: (
type: DirectoryType,
config:
| LdapConfiguration
| GSuiteConfiguration
| AzureConfiguration
| OktaConfiguration
| OneLoginConfiguration
) => Promise<any>;
getLdapKey: (options?: StorageOptions) => Promise<string>;
setLdapKey: (value: string, options?: StorageOptions) => Promise<void>;
getGsuiteKey: (options?: StorageOptions) => Promise<string>;
setGsuiteKey: (value: string, options?: StorageOptions) => Promise<void>;
getAzureKey: (options?: StorageOptions) => Promise<string>;
setAzureKey: (value: string, options?: StorageOptions) => Promise<void>;
getOktaKey: (options?: StorageOptions) => Promise<string>;
setOktaKey: (value: string, options?: StorageOptions) => Promise<void>;
getOneLoginKey: (options?: StorageOptions) => Promise<string>;
setOneLoginKey: (value: string, options?: StorageOptions) => Promise<void>;
getLdapConfiguration: (options?: StorageOptions) => Promise<LdapConfiguration>;
setLdapConfiguration: (value: LdapConfiguration, options?: StorageOptions) => Promise<void>;
getGsuiteConfiguration: (options?: StorageOptions) => Promise<GSuiteConfiguration>;
setGsuiteConfiguration: (value: GSuiteConfiguration, options?: StorageOptions) => Promise<void>;
getAzureConfiguration: (options?: StorageOptions) => Promise<AzureConfiguration>;
setAzureConfiguration: (value: AzureConfiguration, options?: StorageOptions) => Promise<void>;
getOktaConfiguration: (options?: StorageOptions) => Promise<OktaConfiguration>;
setOktaConfiguration: (value: OktaConfiguration, options?: StorageOptions) => Promise<void>;
getOneLoginConfiguration: (options?: StorageOptions) => Promise<OneLoginConfiguration>;
setOneLoginConfiguration: (
value: OneLoginConfiguration,
options?: StorageOptions
) => Promise<void>;
getOrganizationId: (options?: StorageOptions) => Promise<string>;
setOrganizationId: (value: string, options?: StorageOptions) => Promise<void>;
getSync: (options?: StorageOptions) => Promise<SyncConfiguration>;
setSync: (value: SyncConfiguration, options?: StorageOptions) => Promise<void>;
getDirectoryType: (options?: StorageOptions) => Promise<DirectoryType>;
setDirectoryType: (value: DirectoryType, options?: StorageOptions) => Promise<void>;
getUserDelta: (options?: StorageOptions) => Promise<string>;
setUserDelta: (value: string, options?: StorageOptions) => Promise<void>;
getLastUserSync: (options?: StorageOptions) => Promise<Date>;
setLastUserSync: (value: Date, options?: StorageOptions) => Promise<void>;
getLastGroupSync: (options?: StorageOptions) => Promise<Date>;
setLastGroupSync: (value: Date, options?: StorageOptions) => Promise<void>;
getGroupDelta: (options?: StorageOptions) => Promise<string>;
setGroupDelta: (value: string, options?: StorageOptions) => Promise<void>;
getLastSyncHash: (options?: StorageOptions) => Promise<string>;
setLastSyncHash: (value: string, options?: StorageOptions) => Promise<void>;
getSyncingDir: (options?: StorageOptions) => Promise<boolean>;
setSyncingDir: (value: boolean, options?: StorageOptions) => Promise<void>;
clearSyncSettings: (syncHashToo: boolean) => Promise<void>;
}

View File

@@ -1,47 +1,60 @@
<div class="container-fluid">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<p class="text-center font-weight-bold">{{'welcome' | i18n}}</p>
<p class="text-center">{{'logInDesc' | i18n}}</p>
<div class="card">
<h5 class="card-header">{{'logIn' | i18n}}</h5>
<div class="card-body">
<div class="form-group">
<label for="client_id">{{'clientId' | i18n}}</label>
<input id="client_id" name="ClientId" [(ngModel)]="clientId"
class="form-control">
</div>
<div class="form-group">
<div class="row-main">
<label for="client_secret">{{'clientSecret' | i18n}}</label>
<div class="input-group">
<input type="{{showSecret ? 'text' : 'password'}}" id="client_secret" name="ClientSecret"
[(ngModel)]="clientSecret" class="form-control">
<div class="input-group-append">
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleSecret()">
<i class="fa fa-lg" aria-hidden="true"[ngClass]="showSecret ? 'fa-eye-slash' : 'fa-eye'"></i>
</button>
</div>
</div>
</div>
</div>
<div class="d-flex">
<div>
<button type="submit" class="btn btn-primary" [disabled]="form.loading">
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!form.loading"></i>
<i class="fa fa-sign-in fa-fw" [hidden]="form.loading"></i>
{{'logIn' | i18n}}
</button>
</div>
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
{{'settings' | i18n}}
</button>
</div>
</div>
</div>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<p class="text-center font-weight-bold">{{ "welcome" | i18n }}</p>
<p class="text-center">{{ "logInDesc" | i18n }}</p>
<div class="card">
<h5 class="card-header">{{ "logIn" | i18n }}</h5>
<div class="card-body">
<div class="form-group">
<label for="client_id">{{ "clientId" | i18n }}</label>
<input id="client_id" name="ClientId" [(ngModel)]="clientId" class="form-control" />
</div>
<div class="form-group">
<div class="row-main">
<label for="client_secret">{{ "clientSecret" | i18n }}</label>
<div class="input-group">
<input
type="{{ showSecret ? 'text' : 'password' }}"
id="client_secret"
name="ClientSecret"
[(ngModel)]="clientSecret"
class="form-control"
/>
<div class="input-group-append">
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleSecret()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showSecret ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="d-flex">
<div>
<button type="submit" class="btn btn-primary" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!form.loading"></i>
<i class="bwi bwi-sign-in bwi-fw" [hidden]="form.loading"></i>
{{ "logIn" | i18n }}
</button>
</div>
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
{{ "settings" | i18n }}
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</form>
</div>
<ng-template #environment></ng-template>

View File

@@ -1,84 +1,103 @@
import {
Component,
ComponentFactoryResolver,
Input,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { Router } from '@angular/router';
import { Component, Input, ViewChild, ViewContainerRef } from "@angular/core";
import { Router } from "@angular/router";
import { EnvironmentComponent } from './environment.component';
import { ModalService } from "jslib-angular/services/modal.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { Utils } from "jslib-common/misc/utils";
import { ApiLogInCredentials } from "jslib-common/models/domain/logInCredentials";
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from "../../abstractions/state.service";
import { ModalService } from 'jslib-angular/services/modal.service';
import { Utils } from 'jslib-common/misc/utils';
import { ConfigurationService } from '../../services/configuration.service';
import { EnvironmentComponent } from "./environment.component";
@Component({
selector: 'app-apiKey',
templateUrl: 'apiKey.component.html',
selector: "app-apiKey",
templateUrl: "apiKey.component.html",
})
export class ApiKeyComponent {
@ViewChild('environment', { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef;
@Input() clientId: string = '';
@Input() clientSecret: string = '';
@ViewChild("environment", { read: ViewContainerRef, static: true })
environmentModal: ViewContainerRef;
@Input() clientId = "";
@Input() clientSecret = "";
formPromise: Promise<any>;
successRoute = '/tabs/dashboard';
showSecret: boolean = false;
formPromise: Promise<any>;
successRoute = "/tabs/dashboard";
showSecret = false;
constructor(private authService: AuthService, private apiKeyService: ApiKeyService, private router: Router,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private configurationService: ConfigurationService, private platformUtilsService: PlatformUtilsService,
private modalService: ModalService) { }
constructor(
private authService: AuthService,
private router: Router,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private modalService: ModalService,
private logService: LogService,
private stateService: StateService
) {}
async submit() {
if (this.clientId == null || this.clientId === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('clientIdRequired'));
return;
}
if (!this.clientId.startsWith('organization')) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('orgApiKeyRequired'));
return;
}
if (this.clientSecret == null || this.clientSecret === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('clientSecretRequired'));
return;
}
const idParts = this.clientId.split('.');
async submit() {
if (this.clientId == null || this.clientId === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("clientIdRequired")
);
return;
}
if (!this.clientId.startsWith("organization")) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("orgApiKeyRequired")
);
return;
}
if (this.clientSecret == null || this.clientSecret === "") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("clientSecretRequired")
);
return;
}
const idParts = this.clientId.split(".");
if (idParts.length !== 2 || idParts[0] !== 'organization' || !Utils.isGuid(idParts[1])) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidClientId'));
return;
}
try {
this.formPromise = this.authService.logInApiKey(this.clientId, this.clientSecret);
await this.formPromise;
const organizationId = await this.apiKeyService.getEntityId();
await this.configurationService.saveOrganizationId(organizationId);
this.router.navigate([this.successRoute]);
} catch { }
if (idParts.length !== 2 || idParts[0] !== "organization" || !Utils.isGuid(idParts[1])) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("invalidClientId")
);
return;
}
async settings() {
const [modalRef, childComponent] = await this.modalService.openViewRef(EnvironmentComponent, this.environmentModal);
try {
this.formPromise = this.authService.logIn(
new ApiLogInCredentials(this.clientId, this.clientSecret)
);
await this.formPromise;
const organizationId = await this.stateService.getEntityId();
await this.stateService.setOrganizationId(organizationId);
this.router.navigate([this.successRoute]);
} catch (e) {
this.logService.error(e);
}
}
childComponent.onSaved.subscribe(() => {
modalRef.close();
});
}
toggleSecret() {
this.showSecret = !this.showSecret;
document.getElementById('client_secret').focus();
}
async settings() {
const [modalRef, childComponent] = await this.modalService.openViewRef(
EnvironmentComponent,
this.environmentModal
);
childComponent.onSaved.subscribe(() => {
modalRef.close();
});
}
toggleSecret() {
this.showSecret = !this.showSecret;
document.getElementById("client_secret").focus();
}
}

View File

@@ -1,43 +1,61 @@
<div class="modal fade">
<div class="modal-dialog">
<form class="modal-content" (ngSubmit)="submit()">
<div class="modal-header">
<h3 class="modal-title">{{'settings' | i18n}}</h3>
<button type="button" class="close" data-dismiss="modal" title="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h4>{{'selfHostedEnvironment' | i18n}}</h4>
<p>{{'selfHostedEnvironmentFooter' | i18n}}</p>
<div class="form-group">
<label for="baseUrl">{{'baseUrl' | i18n}}</label>
<input id="baseUrl" type="text" name="BaseUrl" [(ngModel)]="baseUrl" class="form-control">
<small class="text-muted form-text">{{'ex' | i18n}} https://bitwarden.company.com</small>
</div>
<h4>{{'customEnvironment' | i18n}}</h4>
<p>{{'customEnvironmentFooter' | i18n}}</p>
<div class="form-group">
<label for="webVaultUrl">{{'webVaultUrl' | i18n}}</label>
<input id="webVaultUrl" type="text" name="WebVaultUrl" [(ngModel)]="webVaultUrl"
class="form-control">
</div>
<div class="form-group">
<label for="apiUrl">{{'apiUrl' | i18n}}</label>
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control">
</div>
<div class="form-group">
<label for="identityUrl">{{'identityUrl' | i18n}}</label>
<input id="identityUrl" type="text" name="IdentityUrl" [(ngModel)]="identityUrl"
class="form-control">
</div>
</div>
<div class="modal-footer justify-content-start">
<button type="submit" class="btn btn-primary">
<i class="fa fa-save fa-fw"></i>
{{'save' | i18n}}
</button>
</div>
</form>
</div>
<div class="modal-dialog">
<form class="modal-content" (ngSubmit)="submit()">
<div class="modal-header">
<h3 class="modal-title">{{ "settings" | i18n }}</h3>
<button type="button" class="close" data-dismiss="modal" title="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h4>{{ "selfHostedEnvironment" | i18n }}</h4>
<p>{{ "selfHostedEnvironmentFooter" | i18n }}</p>
<div class="form-group">
<label for="baseUrl">{{ "baseUrl" | i18n }}</label>
<input
id="baseUrl"
type="text"
name="BaseUrl"
[(ngModel)]="baseUrl"
class="form-control"
/>
<small class="text-muted form-text"
>{{ "ex" | i18n }} https://bitwarden.company.com</small
>
</div>
<h4>{{ "customEnvironment" | i18n }}</h4>
<p>{{ "customEnvironmentFooter" | i18n }}</p>
<div class="form-group">
<label for="webVaultUrl">{{ "webVaultUrl" | i18n }}</label>
<input
id="webVaultUrl"
type="text"
name="WebVaultUrl"
[(ngModel)]="webVaultUrl"
class="form-control"
/>
</div>
<div class="form-group">
<label for="apiUrl">{{ "apiUrl" | i18n }}</label>
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control" />
</div>
<div class="form-group">
<label for="identityUrl">{{ "identityUrl" | i18n }}</label>
<input
id="identityUrl"
type="text"
name="IdentityUrl"
[(ngModel)]="identityUrl"
class="form-control"
/>
</div>
</div>
<div class="modal-footer justify-content-start">
<button type="submit" class="btn btn-primary">
<i class="bwi bwi-save bwi-fw"></i>
{{ "save" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,18 +1,20 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib-angular/components/environment.component';
import { EnvironmentComponent as BaseEnvironmentComponent } from "jslib-angular/components/environment.component";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({
selector: 'app-environment',
templateUrl: 'environment.component.html',
selector: "app-environment",
templateUrl: "environment.component.html",
})
export class EnvironmentComponent extends BaseEnvironmentComponent {
constructor(environmentService: EnvironmentService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService) {
super(platformUtilsService, environmentService, i18nService);
}
constructor(
environmentService: EnvironmentService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService
) {
super(platformUtilsService, environmentService, i18nService);
}
}

View File

@@ -1,58 +1,56 @@
import { NgModule } from '@angular/core';
import {
RouterModule,
Routes,
} from '@angular/router';
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuardService } from './services/auth-guard.service';
import { LaunchGuardService } from './services/launch-guard.service';
import { ApiKeyComponent } from './accounts/apiKey.component';
import { DashboardComponent } from './tabs/dashboard.component';
import { MoreComponent } from './tabs/more.component';
import { SettingsComponent } from './tabs/settings.component';
import { TabsComponent } from './tabs/tabs.component';
import { ApiKeyComponent } from "./accounts/apiKey.component";
import { AuthGuardService } from "./services/auth-guard.service";
import { LaunchGuardService } from "./services/launch-guard.service";
import { DashboardComponent } from "./tabs/dashboard.component";
import { MoreComponent } from "./tabs/more.component";
import { SettingsComponent } from "./tabs/settings.component";
import { TabsComponent } from "./tabs/tabs.component";
const routes: Routes = [
{ path: '', redirectTo: '/login', pathMatch: 'full' },
{
path: 'login',
component: ApiKeyComponent,
canActivate: [LaunchGuardService],
},
{
path: 'tabs',
component: TabsComponent,
children: [
{
path: '',
redirectTo: '/tabs/dashboard',
pathMatch: 'full',
},
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [AuthGuardService],
},
{
path: 'settings',
component: SettingsComponent,
canActivate: [AuthGuardService],
},
{
path: 'more',
component: MoreComponent,
canActivate: [AuthGuardService],
},
],
},
{ path: "", redirectTo: "/login", pathMatch: "full" },
{
path: "login",
component: ApiKeyComponent,
canActivate: [LaunchGuardService],
},
{
path: "tabs",
component: TabsComponent,
children: [
{
path: "",
redirectTo: "/tabs/dashboard",
pathMatch: "full",
},
{
path: "dashboard",
component: DashboardComponent,
canActivate: [AuthGuardService],
},
{
path: "settings",
component: SettingsComponent,
canActivate: [AuthGuardService],
},
{
path: "more",
component: MoreComponent,
canActivate: [AuthGuardService],
},
],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
useHash: true,
/*enableTracing: true,*/
})],
exports: [RouterModule],
imports: [
RouterModule.forRoot(routes, {
useHash: true,
/*enableTracing: true,*/
}),
],
exports: [RouterModule],
})
export class AppRoutingModule { }
export class AppRoutingModule {}

View File

@@ -1,172 +1,160 @@
import {
BodyOutputType,
Toast,
ToasterConfig,
ToasterContainerComponent,
ToasterService,
} from 'angular2-toaster';
Component,
NgZone,
OnInit,
SecurityContext,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { Router } from "@angular/router";
import { IndividualConfig, ToastrService } from "ngx-toastr";
import {
Component,
ComponentFactoryResolver,
NgZone,
OnInit,
SecurityContext,
Type,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { AuthService } from "jslib-common/abstractions/auth.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
import { StateService } from "../abstractions/state.service";
import { SyncService } from "../services/sync.service";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ConfigurationService } from '../services/configuration.service';
import { SyncService } from '../services/sync.service';
const BroadcasterSubscriptionId = 'AppComponent';
const BroadcasterSubscriptionId = "AppComponent";
@Component({
selector: 'app-root',
styles: [],
template: `
<toaster-container [toasterconfig]="toasterConfig"></toaster-container>
<ng-template #settings></ng-template>
<router-outlet></router-outlet>`,
selector: "app-root",
styles: [],
template: ` <ng-template #settings></ng-template>
<router-outlet></router-outlet>`,
})
export class AppComponent implements OnInit {
@ViewChild('settings', { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
@ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
toasterConfig: ToasterConfig = new ToasterConfig({
showCloseButton: true,
mouseoverTimerStop: true,
animation: 'flyRight',
limit: 5,
});
constructor(
private broadcasterService: BroadcasterService,
private tokenService: TokenService,
private authService: AuthService,
private router: Router,
private toastrService: ToastrService,
private i18nService: I18nService,
private sanitizer: DomSanitizer,
private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService,
private syncService: SyncService,
private stateService: StateService,
private logService: LogService
) {}
private lastActivity: number = null;
ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "syncScheduleStarted":
case "syncScheduleStopped":
this.stateService.setSyncingDir(message.command === "syncScheduleStarted");
break;
case "logout":
this.logOut(!!message.expired);
break;
case "checkDirSync":
try {
const syncConfig = await this.stateService.getSync();
if (syncConfig.interval == null || syncConfig.interval < 5) {
return;
}
constructor(private broadcasterService: BroadcasterService, private userService: UserService,
private tokenService: TokenService, private storageService: StorageService,
private authService: AuthService, private router: Router,
private toasterService: ToasterService, private i18nService: I18nService,
private sanitizer: DomSanitizer, private ngZone: NgZone,
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
private configurationService: ConfigurationService, private syncService: SyncService,
private stateService: StateService, private apiService: ApiService) {
(window as any).BitwardenToasterService = toasterService;
}
ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'syncScheduleStarted':
case 'syncScheduleStopped':
this.stateService.save('syncingDir', message.command === 'syncScheduleStarted');
break;
case 'logout':
this.logOut(!!message.expired);
break;
case 'checkDirSync':
try {
const syncConfig = await this.configurationService.getSync();
if (syncConfig.interval == null || syncConfig.interval < 5) {
return;
}
const syncInterval = syncConfig.interval * 60000;
const lastGroupSync = await this.configurationService.getLastGroupSyncDate();
const lastUserSync = await this.configurationService.getLastUserSyncDate();
let lastSync: Date = null;
if (lastGroupSync != null && lastUserSync == null) {
lastSync = lastGroupSync;
} else if (lastGroupSync == null && lastUserSync != null) {
lastSync = lastUserSync;
} else if (lastGroupSync != null && lastUserSync != null) {
if (lastGroupSync.getTime() < lastUserSync.getTime()) {
lastSync = lastGroupSync;
} else {
lastSync = lastUserSync;
}
}
let lastSyncAgo = syncInterval + 1;
if (lastSync != null) {
lastSyncAgo = new Date().getTime() - lastSync.getTime();
}
if (lastSyncAgo >= syncInterval) {
await this.syncService.sync(false, false);
}
} catch { }
this.messagingService.send('scheduleNextDirSync');
break;
case 'showToast':
this.showToast(message);
break;
case 'ssoCallback':
this.router.navigate(['sso'], { queryParams: { code: message.code, state: message.state } });
break;
default:
const syncInterval = syncConfig.interval * 60000;
const lastGroupSync = await this.stateService.getLastGroupSync();
const lastUserSync = await this.stateService.getLastUserSync();
let lastSync: Date = null;
if (lastGroupSync != null && lastUserSync == null) {
lastSync = lastGroupSync;
} else if (lastGroupSync == null && lastUserSync != null) {
lastSync = lastUserSync;
} else if (lastGroupSync != null && lastUserSync != null) {
if (lastGroupSync.getTime() < lastUserSync.getTime()) {
lastSync = lastGroupSync;
} else {
lastSync = lastUserSync;
}
}
let lastSyncAgo = syncInterval + 1;
if (lastSync != null) {
lastSyncAgo = new Date().getTime() - lastSync.getTime();
}
if (lastSyncAgo >= syncInterval) {
await this.syncService.sync(false, false);
}
} catch (e) {
this.logService.error(e);
}
this.messagingService.send("scheduleNextDirSync");
break;
case "showToast":
this.showToast(message);
break;
case "ssoCallback":
this.router.navigate(["sso"], {
queryParams: { code: message.code, state: message.state },
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
private async logOut(expired: boolean) {
const userId = await this.userService.getUserId();
await this.tokenService.clearToken();
await this.userService.clear();
this.authService.logOut(async () => {
if (expired) {
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
this.i18nService.t('loginExpired'));
}
this.router.navigate(['login']);
});
}
private showToast(msg: any) {
const toast: Toast = {
type: msg.type,
title: msg.title,
};
if (typeof (msg.text) === 'string') {
toast.body = msg.text;
} else if (msg.text.length === 1) {
toast.body = msg.text[0];
} else {
let message = '';
msg.text.forEach((t: string) =>
message += ('<p>' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '</p>'));
toast.body = message;
toast.bodyOutputType = BodyOutputType.TrustedHtml;
break;
default:
}
if (msg.options != null) {
if (msg.options.trustedHtml === true) {
toast.bodyOutputType = BodyOutputType.TrustedHtml;
}
if (msg.options.timeout != null && msg.options.timeout > 0) {
toast.timeout = msg.options.timeout;
}
}
this.toasterService.popAsync(toast);
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
private async logOut(expired: boolean) {
await this.tokenService.clearToken();
await this.stateService.clean();
this.authService.logOut(async () => {
if (expired) {
this.platformUtilsService.showToast(
"warning",
this.i18nService.t("loggedOut"),
this.i18nService.t("loginExpired")
);
}
this.router.navigate(["login"]);
});
}
private showToast(msg: any) {
let message = "";
const options: Partial<IndividualConfig> = {};
if (typeof msg.text === "string") {
message = msg.text;
} else if (msg.text.length === 1) {
message = msg.text[0];
} else {
msg.text.forEach(
(t: string) =>
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>")
);
options.enableHtml = true;
}
if (msg.options != null) {
if (msg.options.trustedHtml === true) {
options.enableHtml = true;
}
if (msg.options.timeout != null && msg.options.timeout > 0) {
options.timeOut = msg.options.timeout;
}
}
this.toastrService.show(message, msg.title, options, "toast-" + msg.type);
}
}

View File

@@ -1,71 +1,70 @@
import 'core-js/stable';
import 'zone.js/dist/zone';
import "core-js/stable";
import "zone.js/dist/zone";
import { ToasterModule } from 'angular2-toaster';
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { AppRoutingModule } from './app-routing.module';
import { ServicesModule } from './services/services.module';
import { CalloutComponent } from "jslib-angular/components/callout.component";
import { IconComponent } from "jslib-angular/components/icon.component";
import { BitwardenToastModule } from "jslib-angular/components/toastr.component";
import { A11yTitleDirective } from "jslib-angular/directives/a11y-title.directive";
import { ApiActionDirective } from "jslib-angular/directives/api-action.directive";
import { AutofocusDirective } from "jslib-angular/directives/autofocus.directive";
import { BlurClickDirective } from "jslib-angular/directives/blur-click.directive";
import { BoxRowDirective } from "jslib-angular/directives/box-row.directive";
import { FallbackSrcDirective } from "jslib-angular/directives/fallback-src.directive";
import { StopClickDirective } from "jslib-angular/directives/stop-click.directive";
import { StopPropDirective } from "jslib-angular/directives/stop-prop.directive";
import { I18nPipe } from "jslib-angular/pipes/i18n.pipe";
import { SearchCiphersPipe } from "jslib-angular/pipes/search-ciphers.pipe";
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { CalloutComponent } from 'jslib-angular/components/callout.component';
import { IconComponent } from 'jslib-angular/components/icon.component';
import { ApiKeyComponent } from './accounts/apiKey.component';
import { EnvironmentComponent } from './accounts/environment.component';
import { DashboardComponent } from './tabs/dashboard.component';
import { MoreComponent } from './tabs/more.component';
import { SettingsComponent } from './tabs/settings.component';
import { TabsComponent } from './tabs/tabs.component';
import { A11yTitleDirective } from 'jslib-angular/directives/a11y-title.directive';
import { ApiActionDirective } from 'jslib-angular/directives/api-action.directive';
import { AutofocusDirective } from 'jslib-angular/directives/autofocus.directive';
import { BlurClickDirective } from 'jslib-angular/directives/blur-click.directive';
import { BoxRowDirective } from 'jslib-angular/directives/box-row.directive';
import { FallbackSrcDirective } from 'jslib-angular/directives/fallback-src.directive';
import { StopClickDirective } from 'jslib-angular/directives/stop-click.directive';
import { StopPropDirective } from 'jslib-angular/directives/stop-prop.directive';
import { I18nPipe } from 'jslib-angular/pipes/i18n.pipe';
import { SearchCiphersPipe } from 'jslib-angular/pipes/search-ciphers.pipe';
import { ApiKeyComponent } from "./accounts/apiKey.component";
import { EnvironmentComponent } from "./accounts/environment.component";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { ServicesModule } from "./services/services.module";
import { DashboardComponent } from "./tabs/dashboard.component";
import { MoreComponent } from "./tabs/more.component";
import { SettingsComponent } from "./tabs/settings.component";
import { TabsComponent } from "./tabs/tabs.component";
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
AppRoutingModule,
ServicesModule,
ToasterModule.forRoot(),
],
declarations: [
A11yTitleDirective,
ApiActionDirective,
ApiKeyComponent,
AppComponent,
AutofocusDirective,
BlurClickDirective,
BoxRowDirective,
CalloutComponent,
DashboardComponent,
EnvironmentComponent,
FallbackSrcDirective,
I18nPipe,
IconComponent,
MoreComponent,
SearchCiphersPipe,
SettingsComponent,
StopClickDirective,
StopPropDirective,
TabsComponent,
],
providers: [],
bootstrap: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
AppRoutingModule,
ServicesModule,
BitwardenToastModule.forRoot({
maxOpened: 5,
autoDismiss: true,
closeButton: true,
}),
],
declarations: [
A11yTitleDirective,
ApiActionDirective,
ApiKeyComponent,
AppComponent,
AutofocusDirective,
BlurClickDirective,
BoxRowDirective,
CalloutComponent,
DashboardComponent,
EnvironmentComponent,
FallbackSrcDirective,
I18nPipe,
IconComponent,
MoreComponent,
SearchCiphersPipe,
SettingsComponent,
StopClickDirective,
StopPropDirective,
TabsComponent,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule { }
export class AppModule {}

View File

@@ -1,15 +1,15 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { isDev } from 'jslib-electron/utils';
import { isDev } from "jslib-electron/utils";
// tslint:disable-next-line
require('../scss/styles.scss');
require("../scss/styles.scss");
import { AppModule } from './app.module';
import { AppModule } from "./app.module";
if (!isDev()) {
enableProdMode();
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });

View File

@@ -1,24 +1,21 @@
import { Injectable } from '@angular/core';
import {
CanActivate,
Router,
} from '@angular/router';
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { Injectable } from "@angular/core";
import { CanActivate } from "@angular/router";
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { StateService } from "../../abstractions/state.service";
@Injectable()
export class AuthGuardService implements CanActivate {
constructor(private apiKeyService: ApiKeyService, private router: Router,
private messagingService: MessagingService) { }
constructor(private stateService: StateService, private messagingService: MessagingService) {}
async canActivate() {
const isAuthed = await this.apiKeyService.isAuthenticated();
if (!isAuthed) {
this.messagingService.send('logout');
return false;
}
return true;
async canActivate() {
const isAuthed = await this.stateService.getIsAuthenticated();
if (!isAuthed) {
this.messagingService.send("logout");
return false;
}
return true;
}
}

View File

@@ -1,22 +1,19 @@
import { Injectable } from '@angular/core';
import {
CanActivate,
Router,
} from '@angular/router';
import { Injectable } from "@angular/core";
import { CanActivate, Router } from "@angular/router";
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { StateService } from "../../abstractions/state.service";
@Injectable()
export class LaunchGuardService implements CanActivate {
constructor(private apiKeyService: ApiKeyService, private router: Router) { }
constructor(private stateService: StateService, private router: Router) {}
async canActivate() {
const isAuthed = await this.apiKeyService.isAuthenticated();
if (!isAuthed) {
return true;
}
this.router.navigate(['/tabs/dashboard']);
return false;
async canActivate() {
const isAuthed = await this.stateService.getIsAuthenticated();
if (!isAuthed) {
return true;
}
this.router.navigate(["/tabs/dashboard"]);
return false;
}
}

View File

@@ -1,164 +1,229 @@
import {
APP_INITIALIZER,
NgModule,
} from '@angular/core';
import { APP_INITIALIZER, Injector, NgModule } from "@angular/core";
import { ToasterModule } from 'angular2-toaster';
import { JslibServicesModule } from "jslib-angular/services/jslib-services.module";
import { ApiService as ApiServiceAbstraction } from "jslib-common/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "jslib-common/abstractions/appId.service";
import { AuthService as AuthServiceAbstraction } from "jslib-common/abstractions/auth.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "jslib-common/abstractions/broadcaster.service";
import { CryptoService as CryptoServiceAbstraction } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "jslib-common/abstractions/environment.service";
import { I18nService as I18nServiceAbstraction } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "jslib-common/abstractions/keyConnector.service";
import { LogService as LogServiceAbstraction } from "jslib-common/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "jslib-common/abstractions/platformUtils.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "jslib-common/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "jslib-common/abstractions/storage.service";
import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "jslib-common/abstractions/twoFactor.service";
import { StateFactory } from "jslib-common/factories/stateFactory";
import { GlobalState } from "jslib-common/models/domain/globalState";
import { ContainerService } from "jslib-common/services/container.service";
import { ElectronLogService } from "jslib-electron/services/electronLog.service";
import { ElectronPlatformUtilsService } from "jslib-electron/services/electronPlatformUtils.service";
import { ElectronRendererMessagingService } from "jslib-electron/services/electronRendererMessaging.service";
import { ElectronRendererSecureStorageService } from "jslib-electron/services/electronRendererSecureStorage.service";
import { ElectronRendererStorageService } from "jslib-electron/services/electronRendererStorage.service";
import { NodeApiService } from "jslib-node/services/nodeApi.service";
import { NodeCryptoFunctionService } from "jslib-node/services/nodeCryptoFunction.service";
import { ElectronLogService } from 'jslib-electron/services/electronLog.service';
import { ElectronPlatformUtilsService } from 'jslib-electron/services/electronPlatformUtils.service';
import { ElectronRendererMessagingService } from 'jslib-electron/services/electronRendererMessaging.service';
import { ElectronRendererSecureStorageService } from 'jslib-electron/services/electronRendererSecureStorage.service';
import { ElectronRendererStorageService } from 'jslib-electron/services/electronRendererStorage.service';
import { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
import { Account } from "../../models/account";
import { refreshToken } from "../../services/api.service";
import { AuthService } from "../../services/auth.service";
import { I18nService } from "../../services/i18n.service";
import { NoopTwoFactorService } from "../../services/noop/noopTwoFactor.service";
import { StateService } from "../../services/state.service";
import { StateMigrationService } from "../../services/stateMigration.service";
import { SyncService } from "../../services/sync.service";
import { AuthGuardService } from './auth-guard.service';
import { LaunchGuardService } from './launch-guard.service';
import { AuthGuardService } from "./auth-guard.service";
import { LaunchGuardService } from "./launch-guard.service";
import { ConfigurationService } from '../../services/configuration.service';
import { I18nService } from '../../services/i18n.service';
import { SyncService } from '../../services/sync.service';
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
import { ModalService } from 'jslib-angular/services/modal.service';
import { ValidationService } from 'jslib-angular/services/validation.service';
import { ApiKeyService } from 'jslib-common/services/apiKey.service';
import { AppIdService } from 'jslib-common/services/appId.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
import { ContainerService } from 'jslib-common/services/container.service';
import { CryptoService } from 'jslib-common/services/crypto.service';
import { EnvironmentService } from 'jslib-common/services/environment.service';
import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service';
import { PolicyService } from 'jslib-common/services/policy.service';
import { StateService } from 'jslib-common/services/state.service';
import { TokenService } from 'jslib-common/services/token.service';
import { UserService } from 'jslib-common/services/user.service';
import { NodeCryptoFunctionService } from 'jslib-node/services/nodeCryptoFunction.service';
import { ApiService as ApiServiceAbstraction } from 'jslib-common/abstractions/api.service';
import { ApiKeyService as ApiKeyServiceAbstraction } from 'jslib-common/abstractions/apiKey.service';
import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service';
import { CryptoService as CryptoServiceAbstraction } from 'jslib-common/abstractions/crypto.service';
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib-common/abstractions/cryptoFunction.service';
import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib-common/abstractions/environment.service';
import { I18nService as I18nServiceAbstraction } from 'jslib-common/abstractions/i18n.service';
import { LogService as LogServiceAbstraction } from 'jslib-common/abstractions/log.service';
import { MessagingService as MessagingServiceAbstraction } from 'jslib-common/abstractions/messaging.service';
import {
PasswordGenerationService as PasswordGenerationServiceAbstraction,
} from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService as PolicyServiceAbstraction } from 'jslib-common/abstractions/policy.service';
import { StateService as StateServiceAbstraction } from 'jslib-common/abstractions/state.service';
import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service';
import { TokenService as TokenServiceAbstraction } from 'jslib-common/abstractions/token.service';
import { UserService as UserServiceAbstraction } from 'jslib-common/abstractions/user.service';
import { ApiService, refreshToken } from '../../services/api.service';
import { AuthService } from '../../services/auth.service';
const logService = new ElectronLogService();
const i18nService = new I18nService(window.navigator.language, './locales');
const stateService = new StateService();
const broadcasterService = new BroadcasterService();
const messagingService = new ElectronRendererMessagingService(broadcasterService);
const storageService: StorageServiceAbstraction = new ElectronRendererStorageService();
const platformUtilsService = new ElectronPlatformUtilsService(i18nService, messagingService, false, storageService);
const secureStorageService: StorageServiceAbstraction = new ElectronRendererSecureStorageService();
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new NodeCryptoFunctionService();
const cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService,
platformUtilsService, logService);
const appIdService = new AppIdService(storageService);
const tokenService = new TokenService(storageService);
const environmentService = new EnvironmentService(storageService);
const apiService = new ApiService(tokenService, platformUtilsService, environmentService, refreshTokenCallback,
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
const userService = new UserService(tokenService, storageService);
const apiKeyService = new ApiKeyService(tokenService, storageService);
const containerService = new ContainerService(cryptoService);
const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
i18nService, platformUtilsService, messagingService, null, logService, apiKeyService, false);
const configurationService = new ConfigurationService(storageService, secureStorageService);
const syncService = new SyncService(configurationService, logService, cryptoFunctionService, apiService,
messagingService, i18nService, environmentService);
const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, null);
const policyService = new PolicyService(userService, storageService);
containerService.attachToWindow(window);
function refreshTokenCallback(): Promise<any> {
return refreshToken(apiKeyService, authService);
function refreshTokenCallback(injector: Injector) {
return () => {
const stateService = injector.get(StateServiceAbstraction);
const authService = injector.get(AuthServiceAbstraction);
return refreshToken(stateService, authService);
};
}
export function initFactory(): Function {
return async () => {
await environmentService.setUrlsFromStorage();
await i18nService.init();
authService.init();
const htmlEl = window.document.documentElement;
htmlEl.classList.add('os_' + platformUtilsService.getDeviceString());
htmlEl.classList.add('locale_' + i18nService.translationLocale);
window.document.title = i18nService.t('bitwardenDirectoryConnector');
export function initFactory(
environmentService: EnvironmentServiceAbstraction,
i18nService: I18nService,
platformUtilsService: PlatformUtilsServiceAbstraction,
stateService: StateServiceAbstraction,
cryptoService: CryptoServiceAbstraction
): () => Promise<void> {
return async () => {
await stateService.init();
await environmentService.setUrlsFromStorage();
await i18nService.init();
const htmlEl = window.document.documentElement;
htmlEl.classList.add("os_" + platformUtilsService.getDeviceString());
htmlEl.classList.add("locale_" + i18nService.translationLocale);
window.document.title = i18nService.t("bitwardenDirectoryConnector");
let installAction = null;
const installedVersion = await storageService.get<string>(ConstantsService.installedVersionKey);
const currentVersion = await platformUtilsService.getApplicationVersion();
if (installedVersion == null) {
installAction = 'install';
} else if (installedVersion !== currentVersion) {
installAction = 'update';
}
let installAction = null;
const installedVersion = await stateService.getInstalledVersion();
const currentVersion = await platformUtilsService.getApplicationVersion();
if (installedVersion == null) {
installAction = "install";
} else if (installedVersion !== currentVersion) {
installAction = "update";
}
if (installAction != null) {
await storageService.save(ConstantsService.installedVersionKey, currentVersion);
}
if (installAction != null) {
await stateService.setInstalledVersion(currentVersion);
}
window.setTimeout(async () => {
if (await userService.isAuthenticated()) {
const profile = await apiService.getProfile();
stateService.save('profileOrganizations', profile.organizations);
}
}, 500);
};
const containerService = new ContainerService(cryptoService);
containerService.attachToWindow(window);
};
}
@NgModule({
imports: [
ToasterModule,
],
declarations: [],
providers: [
ValidationService,
AuthGuardService,
LaunchGuardService,
ModalService,
{ provide: AuthServiceAbstraction, useValue: authService },
{ provide: EnvironmentServiceAbstraction, useValue: environmentService },
{ provide: TokenServiceAbstraction, useValue: tokenService },
{ provide: I18nServiceAbstraction, useValue: i18nService },
{ provide: CryptoServiceAbstraction, useValue: cryptoService },
{ provide: PlatformUtilsServiceAbstraction, useValue: platformUtilsService },
{ provide: ApiServiceAbstraction, useValue: apiService },
{ provide: UserServiceAbstraction, useValue: userService },
{ provide: ApiKeyServiceAbstraction, useValue: apiKeyService },
{ provide: MessagingServiceAbstraction, useValue: messagingService },
{ provide: BroadcasterService, useValue: broadcasterService },
{ provide: StorageServiceAbstraction, useValue: storageService },
{ provide: StateServiceAbstraction, useValue: stateService },
{ provide: LogServiceAbstraction, useValue: logService },
{ provide: ConfigurationService, useValue: configurationService },
{ provide: SyncService, useValue: syncService },
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
{ provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService },
{ provide: PolicyServiceAbstraction, useValue: policyService },
{
provide: APP_INITIALIZER,
useFactory: initFactory,
deps: [],
multi: true,
},
],
imports: [JslibServicesModule],
declarations: [],
providers: [
{
provide: APP_INITIALIZER,
useFactory: initFactory,
deps: [
EnvironmentServiceAbstraction,
I18nServiceAbstraction,
PlatformUtilsServiceAbstraction,
StateServiceAbstraction,
CryptoServiceAbstraction,
],
multi: true,
},
{ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] },
{
provide: I18nServiceAbstraction,
useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"),
deps: ["WINDOW"],
},
{
provide: MessagingServiceAbstraction,
useClass: ElectronRendererMessagingService,
deps: [BroadcasterServiceAbstraction],
},
{ provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService },
{ provide: "SECURE_STORAGE", useClass: ElectronRendererSecureStorageService },
{
provide: PlatformUtilsServiceAbstraction,
useFactory: (
i18nService: I18nServiceAbstraction,
messagingService: MessagingServiceAbstraction,
stateService: StateServiceAbstraction
) => new ElectronPlatformUtilsService(i18nService, messagingService, false, stateService),
deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StateServiceAbstraction],
},
{ provide: CryptoFunctionServiceAbstraction, useClass: NodeCryptoFunctionService, deps: [] },
{
provide: ApiServiceAbstraction,
useFactory: (
tokenService: TokenServiceAbstraction,
platformUtilsService: PlatformUtilsServiceAbstraction,
environmentService: EnvironmentServiceAbstraction,
messagingService: MessagingServiceAbstraction,
injector: Injector
) =>
new NodeApiService(
tokenService,
platformUtilsService,
environmentService,
async (expired: boolean) => messagingService.send("logout", { expired: expired }),
"Bitwarden_DC/" +
platformUtilsService.getApplicationVersion() +
" (" +
platformUtilsService.getDeviceString().toUpperCase() +
")",
refreshTokenCallback(injector)
),
deps: [
TokenServiceAbstraction,
PlatformUtilsServiceAbstraction,
EnvironmentServiceAbstraction,
MessagingServiceAbstraction,
Injector,
],
},
{
provide: AuthServiceAbstraction,
useClass: AuthService,
deps: [
CryptoServiceAbstraction,
ApiServiceAbstraction,
TokenServiceAbstraction,
AppIdServiceAbstraction,
PlatformUtilsServiceAbstraction,
MessagingServiceAbstraction,
LogServiceAbstraction,
KeyConnectorServiceAbstraction,
EnvironmentServiceAbstraction,
StateServiceAbstraction,
TwoFactorServiceAbstraction,
I18nServiceAbstraction,
],
},
{
provide: SyncService,
useClass: SyncService,
deps: [
LogServiceAbstraction,
CryptoFunctionServiceAbstraction,
ApiServiceAbstraction,
MessagingServiceAbstraction,
I18nServiceAbstraction,
EnvironmentServiceAbstraction,
StateServiceAbstraction,
],
},
AuthGuardService,
LaunchGuardService,
{
provide: StateMigrationServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction
) =>
new StateMigrationService(
storageService,
secureStorageService,
new StateFactory(GlobalState, Account)
),
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
},
{
provide: StateServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction,
logService: LogServiceAbstraction,
stateMigrationService: StateMigrationServiceAbstraction
) =>
new StateService(
storageService,
secureStorageService,
logService,
stateMigrationService,
true,
new StateFactory(GlobalState, Account)
),
deps: [
StorageServiceAbstraction,
"SECURE_STORAGE",
LogServiceAbstraction,
StateMigrationServiceAbstraction,
],
},
{
provide: TwoFactorServiceAbstraction,
useClass: NoopTwoFactorService,
},
],
})
export class ServicesModule {
}
export class ServicesModule {}

View File

@@ -1,99 +1,110 @@
<div class="card mb-3">
<h3 class="card-header">{{'sync' | i18n}}</h3>
<div class="card-body">
<p>
{{'lastGroupSync' | i18n}}:
<span *ngIf="!lastGroupSync">-</span>
{{lastGroupSync | date:'medium'}}
<br /> {{'lastUserSync' | i18n}}:
<span *ngIf="!lastUserSync">-</span>
{{lastUserSync | date:'medium'}}
</p>
<p>
{{'syncStatus' | i18n}}:
<strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong>
<strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong>
</p>
<form #startForm [appApiAction]="startPromise" class="d-inline">
<button (click)="start()" class="btn btn-primary"
[disabled]="startForm.loading">
<i class="fa fa-play fa-fw" [hidden]="startForm.loading"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startForm.loading"></i>
{{'startSync' | i18n}}
</button>
</form>
<button (click)="stop()" class="btn btn-primary">
<i class="fa fa-stop fa-fw"></i>
{{'stopSync' | i18n}}
</button>
<form #syncForm [appApiAction]="syncPromise" class="d-inline">
<button (click)="sync()" class="btn btn-primary"
[disabled]="syncForm.loading">
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncForm.loading}"></i>
{{'syncNow' | i18n}}
</button>
</form>
</div>
<h3 class="card-header">{{ "sync" | i18n }}</h3>
<div class="card-body">
<p>
{{ "lastGroupSync" | i18n }}:
<span *ngIf="!lastGroupSync">-</span>
{{ lastGroupSync | date: "medium" }}
<br />
{{ "lastUserSync" | i18n }}:
<span *ngIf="!lastUserSync">-</span>
{{ lastUserSync | date: "medium" }}
</p>
<p>
{{ "syncStatus" | i18n }}:
<strong *ngIf="syncRunning" class="text-success">{{ "running" | i18n }}</strong>
<strong *ngIf="!syncRunning" class="text-danger">{{ "stopped" | i18n }}</strong>
</p>
<form #startForm [appApiAction]="startPromise" class="d-inline">
<button (click)="start()" class="btn btn-primary" [disabled]="startForm.loading">
<i class="bwi bwi-play bwi-fw" [hidden]="startForm.loading"></i>
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!startForm.loading"></i>
{{ "startSync" | i18n }}
</button>
</form>
<button (click)="stop()" class="btn btn-primary">
<i class="bwi bwi-stop bwi-fw"></i>
{{ "stopSync" | i18n }}
</button>
<form #syncForm [appApiAction]="syncPromise" class="d-inline">
<button (click)="sync()" class="btn btn-primary" [disabled]="syncForm.loading">
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': syncForm.loading }"></i>
{{ "syncNow" | i18n }}
</button>
</form>
</div>
</div>
<div class="card">
<h3 class="card-header">{{'testing' | i18n}}</h3>
<div class="card-body">
<p>{{'testingDesc' | i18n}}</p>
<form #simForm [appApiAction]="simPromise" class="d-inline">
<button (click)="simulate()" class="btn btn-primary"
[disabled]="simForm.loading">
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simForm.loading"></i>
<i class="fa fa-bug fa-fw" [hidden]="simForm.loading"></i>
{{'testNow' | i18n}}
</button>
</form>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="simSinceLast" [(ngModel)]="simSinceLast">
<label class="form-check-label" for="simSinceLast">{{'testLastSync' | i18n}}</label>
</div>
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
<hr />
<div class="row">
<div class="col-lg">
<h4>{{'users' | i18n}}</h4>
<ul class="fa-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
<li *ngFor="let u of simEnabledUsers" title="{{u.referenceId}}">
<i class="fa-li fa fa-user"></i>
{{u.displayName}}
</li>
</ul>
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">{{'noUsers' | i18n}}</p>
<h4>{{'disabledUsers' | i18n}}</h4>
<ul class="fa-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
<li *ngFor="let u of simDisabledUsers" title="{{u.referenceId}}">
<i class="fa-li fa fa-user"></i>
{{u.displayName}}
</li>
</ul>
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">{{'noUsers' | i18n}}</p>
<h4>{{'deletedUsers' | i18n}}</h4>
<ul class="fa-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
<li *ngFor="let u of simDeletedUsers" title="{{u.referenceId}}">
<i class="fa-li fa fa-user"></i>
{{u.displayName}}
</li>
</ul>
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">{{'noUsers' | i18n}}</p>
</div>
<div class="col-lg">
<h4>{{'groups' | i18n}}</h4>
<ul class="fa-ul testing-list" *ngIf="simGroups && simGroups.length">
<li *ngFor="let g of simGroups" title="{{g.referenceId}}">
<i class="fa-li fa fa-sitemap"></i>
{{g.displayName}}
<ul class="small" *ngIf="g.users && g.users.length">
<li *ngFor="let u of g.users" title="{{u.referenceId}}">{{u.displayName}}</li>
</ul>
</li>
</ul>
<p *ngIf="!simGroups || !simGroups.length">{{'noGroups' | i18n}}</p>
</div>
</div>
</ng-container>
<h3 class="card-header">{{ "testing" | i18n }}</h3>
<div class="card-body">
<p>{{ "testingDesc" | i18n }}</p>
<form #simForm [appApiAction]="simPromise" class="d-inline">
<button (click)="simulate()" class="btn btn-primary" [disabled]="simForm.loading">
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!simForm.loading"></i>
<i class="bwi bwi-bug bwi-fw" [hidden]="simForm.loading"></i>
{{ "testNow" | i18n }}
</button>
</form>
<div class="form-check mt-2">
<input
class="form-check-input"
type="checkbox"
id="simSinceLast"
[(ngModel)]="simSinceLast"
/>
<label class="form-check-label" for="simSinceLast">{{ "testLastSync" | i18n }}</label>
</div>
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
<hr />
<div class="row">
<div class="col-lg">
<h4>{{ "users" | i18n }}</h4>
<ul class="bwi-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
<li *ngFor="let u of simEnabledUsers" title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">
{{ "noUsers" | i18n }}
</p>
<h4>{{ "disabledUsers" | i18n }}</h4>
<ul class="bwi-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
<li *ngFor="let u of simDisabledUsers" title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">
{{ "noUsers" | i18n }}
</p>
<h4>{{ "deletedUsers" | i18n }}</h4>
<ul class="bwi-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
<li *ngFor="let u of simDeletedUsers" title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">
{{ "noUsers" | i18n }}
</p>
</div>
<div class="col-lg">
<h4>{{ "groups" | i18n }}</h4>
<ul class="bwi-ul testing-list" *ngIf="simGroups && simGroups.length">
<li *ngFor="let g of simGroups" title="{{ g.referenceId }}">
<i class="bwi bwi-li bwi-sitemap"></i>
{{ g.displayName }}
<ul class="small" *ngIf="g.users && g.users.length">
<li *ngFor="let u of g.users" title="{{ u.referenceId }}">
{{ u.displayName }}
</li>
</ul>
</li>
</ul>
<p *ngIf="!simGroups || !simGroups.length">{{ "noGroups" | i18n }}</p>
</div>
</div>
</ng-container>
</div>
</div>

View File

@@ -1,123 +1,124 @@
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
} from '@angular/core';
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ToasterService } from 'angular2-toaster';
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { StateService } from "../../abstractions/state.service";
import { GroupEntry } from "../../models/groupEntry";
import { SimResult } from "../../models/simResult";
import { UserEntry } from "../../models/userEntry";
import { SyncService } from "../../services/sync.service";
import { ConnectorUtils } from "../../utils";
import { SyncService } from '../../services/sync.service';
import { GroupEntry } from '../../models/groupEntry';
import { SimResult } from '../../models/simResult';
import { UserEntry } from '../../models/userEntry';
import { ConfigurationService } from '../../services/configuration.service';
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
import { ConnectorUtils } from '../../utils';
const BroadcasterSubscriptionId = 'DashboardComponent';
const BroadcasterSubscriptionId = "DashboardComponent";
@Component({
selector: 'app-dashboard',
templateUrl: 'dashboard.component.html',
selector: "app-dashboard",
templateUrl: "dashboard.component.html",
})
export class DashboardComponent implements OnInit, OnDestroy {
simGroups: GroupEntry[];
simUsers: UserEntry[];
simEnabledUsers: UserEntry[] = [];
simDisabledUsers: UserEntry[] = [];
simDeletedUsers: UserEntry[] = [];
simPromise: Promise<SimResult>;
simSinceLast: boolean = false;
syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
startPromise: Promise<any>;
lastGroupSync: Date;
lastUserSync: Date;
syncRunning: boolean;
simGroups: GroupEntry[];
simUsers: UserEntry[];
simEnabledUsers: UserEntry[] = [];
simDisabledUsers: UserEntry[] = [];
simDeletedUsers: UserEntry[] = [];
simPromise: Promise<SimResult>;
simSinceLast = false;
syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
startPromise: Promise<any>;
lastGroupSync: Date;
lastUserSync: Date;
syncRunning: boolean;
constructor(private i18nService: I18nService, private syncService: SyncService,
private configurationService: ConfigurationService, private broadcasterService: BroadcasterService,
private ngZone: NgZone, private messagingService: MessagingService,
private toasterService: ToasterService, private changeDetectorRef: ChangeDetectorRef,
private stateService: StateService) { }
constructor(
private i18nService: I18nService,
private syncService: SyncService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
private changeDetectorRef: ChangeDetectorRef,
private stateService: StateService
) {}
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'dirSyncCompleted':
this.updateLastSync();
break;
default:
break;
}
this.changeDetectorRef.detectChanges();
});
});
this.syncRunning = !!(await this.stateService.get('syncingDir'));
this.updateLastSync();
}
async ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async start() {
this.startPromise = this.syncService.sync(false, false);
await this.startPromise;
this.messagingService.send('scheduleNextDirSync');
this.syncRunning = true;
this.toasterService.popAsync('success', null, this.i18nService.t('syncingStarted'));
}
async stop() {
this.messagingService.send('cancelDirSync');
this.syncRunning = false;
this.toasterService.popAsync('success', null, this.i18nService.t('syncingStopped'));
}
async sync() {
this.syncPromise = this.syncService.sync(false, false);
const result = await this.syncPromise;
const groupCount = result[0] != null ? result[0].length : 0;
const userCount = result[1] != null ? result[1].length : 0;
this.toasterService.popAsync('success', null,
this.i18nService.t('syncCounts', groupCount.toString(), userCount.toString()));
}
async simulate() {
this.simGroups = [];
this.simUsers = [];
this.simEnabledUsers = [];
this.simDisabledUsers = [];
this.simDeletedUsers = [];
try {
this.simPromise = ConnectorUtils.simulate(this.syncService, this.i18nService, this.simSinceLast);
const result = await this.simPromise;
this.simGroups = result.groups;
this.simUsers = result.users;
this.simEnabledUsers = result.enabledUsers;
this.simDisabledUsers = result.disabledUsers;
this.simDeletedUsers = result.deletedUsers;
} catch (e) {
this.simGroups = null;
this.simUsers = null;
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "dirSyncCompleted":
this.updateLastSync();
break;
default:
break;
}
}
private async updateLastSync() {
this.lastGroupSync = await this.configurationService.getLastGroupSyncDate();
this.lastUserSync = await this.configurationService.getLastUserSyncDate();
this.changeDetectorRef.detectChanges();
});
});
this.syncRunning = !!(await this.stateService.getSyncingDir());
this.updateLastSync();
}
async ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async start() {
this.startPromise = this.syncService.sync(false, false);
await this.startPromise;
this.messagingService.send("scheduleNextDirSync");
this.syncRunning = true;
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingStarted"));
}
async stop() {
this.messagingService.send("cancelDirSync");
this.syncRunning = false;
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingStopped"));
}
async sync() {
this.syncPromise = this.syncService.sync(false, false);
const result = await this.syncPromise;
const groupCount = result[0] != null ? result[0].length : 0;
const userCount = result[1] != null ? result[1].length : 0;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("syncCounts", groupCount.toString(), userCount.toString())
);
}
async simulate() {
this.simGroups = [];
this.simUsers = [];
this.simEnabledUsers = [];
this.simDisabledUsers = [];
this.simDeletedUsers = [];
try {
this.simPromise = ConnectorUtils.simulate(
this.syncService,
this.i18nService,
this.simSinceLast
);
const result = await this.simPromise;
this.simGroups = result.groups;
this.simUsers = result.users;
this.simEnabledUsers = result.enabledUsers;
this.simDisabledUsers = result.disabledUsers;
this.simDeletedUsers = result.deletedUsers;
} catch (e) {
this.simGroups = null;
this.simUsers = null;
}
}
private async updateLastSync() {
this.lastGroupSync = await this.stateService.getLastGroupSync();
this.lastUserSync = await this.stateService.getLastUserSync();
}
}

View File

@@ -1,32 +1,38 @@
<div class="row">
<div class="col-sm">
<div class="card">
<h3 class="card-header">{{'about' | i18n}}</h3>
<div class="card-body">
<p>
{{'bitwardenDirectoryConnector' | i18n}}
<br /> {{'version' | i18n : version}}
<br /> &copy; Bitwarden Inc. LLC 2015-{{year}}
</p>
<button class="btn btn-primary" type="button" (click)="update()" [disabled]="checkingForUpdate">
<i class="fa fa-download fa-fw" [hidden]="checkingForUpdate"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!checkingForUpdate"></i>
{{'checkForUpdates' | i18n}}
</button>
</div>
</div>
<div class="col-sm">
<div class="card">
<h3 class="card-header">{{ "about" | i18n }}</h3>
<div class="card-body">
<p>
{{ "bitwardenDirectoryConnector" | i18n }}
<br />
{{ "version" | i18n: version }} <br />
&copy; Bitwarden Inc. LLC 2015-{{ year }}
</p>
<button
class="btn btn-primary"
type="button"
(click)="update()"
[disabled]="checkingForUpdate"
>
<i class="bwi bwi-download bwi-fw" [hidden]="checkingForUpdate"></i>
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!checkingForUpdate"></i>
{{ "checkForUpdates" | i18n }}
</button>
</div>
</div>
<div class="col-sm">
<div class="card">
<h3 class="card-header">{{'other' | i18n}}</h3>
<div class="card-body">
<button class="btn btn-primary" type="button" (click)="logOut()">
{{'logOut' | i18n}}
</button>
<button class="btn btn-primary" type="button" (click)="clearCache()">
{{'clearSyncCache' | i18n}}
</button>
</div>
</div>
</div>
<div class="col-sm">
<div class="card">
<h3 class="card-header">{{ "other" | i18n }}</h3>
<div class="card-body">
<button class="btn btn-primary" type="button" (click)="logOut()">
{{ "logOut" | i18n }}
</button>
<button class="btn btn-primary" type="button" (click)="clearCache()">
{{ "clearSyncCache" | i18n }}
</button>
</div>
</div>
</div>
</div>

View File

@@ -1,78 +1,77 @@
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
} from '@angular/core';
import { ChangeDetectorRef, Component, NgZone, OnInit } from "@angular/core";
import { ToasterService } from 'angular2-toaster';
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
import { StateService } from "../../abstractions/state.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ConfigurationService } from '../../services/configuration.service';
const BroadcasterSubscriptionId = 'MoreComponent';
const BroadcasterSubscriptionId = "MoreComponent";
@Component({
selector: 'app-more',
templateUrl: 'more.component.html',
selector: "app-more",
templateUrl: "more.component.html",
})
export class MoreComponent implements OnInit {
version: string;
year: string;
checkingForUpdate = false;
version: string;
year: string;
checkingForUpdate = false;
constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private messagingService: MessagingService, private configurationService: ConfigurationService,
private toasterService: ToasterService, private broadcasterService: BroadcasterService,
private ngZone: NgZone, private changeDetectorRef: ChangeDetectorRef) { }
constructor(
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private messagingService: MessagingService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private changeDetectorRef: ChangeDetectorRef,
private stateService: StateService
) {}
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'checkingForUpdate':
this.checkingForUpdate = true;
break;
case 'doneCheckingForUpdate':
this.checkingForUpdate = false;
break;
default:
break;
}
this.changeDetectorRef.detectChanges();
});
});
this.year = new Date().getFullYear().toString();
this.version = await this.platformUtilsService.getApplicationVersion();
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
update() {
this.messagingService.send('checkForUpdate');
}
async logOut() {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('logOutConfirmation'), this.i18nService.t('logOut'),
this.i18nService.t('yes'), this.i18nService.t('cancel'));
if (confirmed) {
this.messagingService.send('logout');
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "checkingForUpdate":
this.checkingForUpdate = true;
break;
case "doneCheckingForUpdate":
this.checkingForUpdate = false;
break;
default:
break;
}
}
async clearCache() {
await this.configurationService.clearStatefulSettings(true);
this.toasterService.popAsync('success', null, this.i18nService.t('syncCacheCleared'));
this.changeDetectorRef.detectChanges();
});
});
this.year = new Date().getFullYear().toString();
this.version = await this.platformUtilsService.getApplicationVersion();
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
update() {
this.messagingService.send("checkForUpdate");
}
async logOut() {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("logOutConfirmation"),
this.i18nService.t("logOut"),
this.i18nService.t("yes"),
this.i18nService.t("cancel")
);
if (confirmed) {
this.messagingService.send("logout");
}
}
async clearCache() {
await this.stateService.clearSyncSettings(true);
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncCacheCleared"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,153 +1,152 @@
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
} from '@angular/core';
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { ProfileOrganizationResponse } from 'jslib-common/models/response/profileOrganizationResponse';
import { ConfigurationService } from '../../services/configuration.service';
import { DirectoryType } from '../../enums/directoryType';
import { AzureConfiguration } from '../../models/azureConfiguration';
import { GSuiteConfiguration } from '../../models/gsuiteConfiguration';
import { LdapConfiguration } from '../../models/ldapConfiguration';
import { OktaConfiguration } from '../../models/oktaConfiguration';
import { OneLoginConfiguration } from '../../models/oneLoginConfiguration';
import { SyncConfiguration } from '../../models/syncConfiguration';
import { ConnectorUtils } from '../../utils';
import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../../enums/directoryType";
import { AzureConfiguration } from "../../models/azureConfiguration";
import { GSuiteConfiguration } from "../../models/gsuiteConfiguration";
import { LdapConfiguration } from "../../models/ldapConfiguration";
import { OktaConfiguration } from "../../models/oktaConfiguration";
import { OneLoginConfiguration } from "../../models/oneLoginConfiguration";
import { SyncConfiguration } from "../../models/syncConfiguration";
import { ConnectorUtils } from "../../utils";
@Component({
selector: 'app-settings',
templateUrl: 'settings.component.html',
selector: "app-settings",
templateUrl: "settings.component.html",
})
export class SettingsComponent implements OnInit, OnDestroy {
directory: DirectoryType;
directoryType = DirectoryType;
ldap = new LdapConfiguration();
gsuite = new GSuiteConfiguration();
azure = new AzureConfiguration();
okta = new OktaConfiguration();
oneLogin = new OneLoginConfiguration();
sync = new SyncConfiguration();
directoryOptions: any[];
showLdapPassword: boolean = false;
showAzureKey: boolean = false;
showOktaKey: boolean = false;
showOneLoginSecret: boolean = false;
directory: DirectoryType;
directoryType = DirectoryType;
ldap = new LdapConfiguration();
gsuite = new GSuiteConfiguration();
azure = new AzureConfiguration();
okta = new OktaConfiguration();
oneLogin = new OneLoginConfiguration();
sync = new SyncConfiguration();
directoryOptions: any[];
showLdapPassword = false;
showAzureKey = false;
showOktaKey = false;
showOneLoginSecret = false;
constructor(private i18nService: I18nService, private configurationService: ConfigurationService,
private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone,
private stateService: StateService) {
this.directoryOptions = [
{ name: i18nService.t('select'), value: null },
{ name: 'Active Directory / LDAP', value: DirectoryType.Ldap },
{ name: 'Azure Active Directory', value: DirectoryType.AzureActiveDirectory },
{ name: 'G Suite (Google)', value: DirectoryType.GSuite },
{ name: 'Okta', value: DirectoryType.Okta },
{ name: 'OneLogin', value: DirectoryType.OneLogin },
];
constructor(
private i18nService: I18nService,
private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone,
private logService: LogService,
private stateService: StateService
) {
this.directoryOptions = [
{ name: this.i18nService.t("select"), value: null },
{ name: "Active Directory / LDAP", value: DirectoryType.Ldap },
{ name: "Azure Active Directory", value: DirectoryType.AzureActiveDirectory },
{ name: "G Suite (Google)", value: DirectoryType.GSuite },
{ name: "Okta", value: DirectoryType.Okta },
{ name: "OneLogin", value: DirectoryType.OneLogin },
];
}
async ngOnInit() {
this.directory = await this.stateService.getDirectoryType();
this.ldap =
(await this.stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) || this.ldap;
this.gsuite =
(await this.stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
this.gsuite;
this.azure =
(await this.stateService.getDirectory<AzureConfiguration>(
DirectoryType.AzureActiveDirectory
)) || this.azure;
this.okta =
(await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) || this.okta;
this.oneLogin =
(await this.stateService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin)) ||
this.oneLogin;
this.sync = (await this.stateService.getSync()) || this.sync;
}
async ngOnDestroy() {
await this.submit();
}
async submit() {
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
if (this.ldap != null && this.ldap.ad) {
this.ldap.pagedSearch = true;
}
await this.stateService.setDirectoryType(this.directory);
await this.stateService.setDirectory(DirectoryType.Ldap, this.ldap);
await this.stateService.setDirectory(DirectoryType.GSuite, this.gsuite);
await this.stateService.setDirectory(DirectoryType.AzureActiveDirectory, this.azure);
await this.stateService.setDirectory(DirectoryType.Okta, this.okta);
await this.stateService.setDirectory(DirectoryType.OneLogin, this.oneLogin);
await this.stateService.setSync(this.sync);
}
parseKeyFile() {
const filePicker = document.getElementById("keyFile") as HTMLInputElement;
if (filePicker.files == null || filePicker.files.length < 0) {
return;
}
async ngOnInit() {
this.directory = await this.configurationService.getDirectoryType();
this.ldap = (await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) ||
this.ldap;
this.gsuite = (await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
this.gsuite;
this.azure = (await this.configurationService.getDirectory<AzureConfiguration>(
DirectoryType.AzureActiveDirectory)) || this.azure;
this.okta = (await this.configurationService.getDirectory<OktaConfiguration>(
DirectoryType.Okta)) || this.okta;
this.oneLogin = (await this.configurationService.getDirectory<OneLoginConfiguration>(
DirectoryType.OneLogin)) || this.oneLogin;
this.sync = (await this.configurationService.getSync()) || this.sync;
}
async ngOnDestroy() {
await this.submit();
}
async submit() {
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
if (this.ldap != null && this.ldap.ad) {
this.ldap.pagedSearch = true;
const reader = new FileReader();
reader.readAsText(filePicker.files[0], "utf-8");
reader.onload = (evt) => {
this.ngZone.run(async () => {
try {
const result = JSON.parse((evt.target as FileReader).result as string);
if (result.client_email != null && result.private_key != null) {
this.gsuite.clientEmail = result.client_email;
this.gsuite.privateKey = result.private_key;
}
} catch (e) {
this.logService.error(e);
}
await this.configurationService.saveDirectoryType(this.directory);
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap);
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite);
await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure);
await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta);
await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin);
await this.configurationService.saveSync(this.sync);
this.changeDetectorRef.detectChanges();
});
// reset file input
// ref: https://stackoverflow.com/a/20552042
filePicker.type = "";
filePicker.type = "file";
filePicker.value = "";
};
}
setSslPath(id: string) {
const filePicker = document.getElementById(id + "_file") as HTMLInputElement;
if (filePicker.files == null || filePicker.files.length < 0) {
return;
}
parseKeyFile() {
const filePicker = (document.getElementById('keyFile') as HTMLInputElement);
if (filePicker.files == null || filePicker.files.length < 0) {
return;
}
(this.ldap as any)[id] = filePicker.files[0].path;
// reset file input
// ref: https://stackoverflow.com/a/20552042
filePicker.type = "";
filePicker.type = "file";
filePicker.value = "";
}
const reader = new FileReader();
reader.readAsText(filePicker.files[0], 'utf-8');
reader.onload = evt => {
this.ngZone.run(async () => {
try {
const result = JSON.parse((evt.target as FileReader).result as string);
if (result.client_email != null && result.private_key != null) {
this.gsuite.clientEmail = result.client_email;
this.gsuite.privateKey = result.private_key;
}
} catch { }
this.changeDetectorRef.detectChanges();
});
toggleLdapPassword() {
this.showLdapPassword = !this.showLdapPassword;
document.getElementById("password").focus();
}
// reset file input
// ref: https://stackoverflow.com/a/20552042
filePicker.type = '';
filePicker.type = 'file';
filePicker.value = '';
};
}
toggleAzureKey() {
this.showAzureKey = !this.showAzureKey;
document.getElementById("secretKey").focus();
}
setSslPath(id: string) {
const filePicker = (document.getElementById(id + '_file') as HTMLInputElement);
if (filePicker.files == null || filePicker.files.length < 0) {
return;
}
toggleOktaKey() {
this.showOktaKey = !this.showOktaKey;
document.getElementById("oktaToken").focus();
}
(this.ldap as any)[id] = filePicker.files[0].path;
// reset file input
// ref: https://stackoverflow.com/a/20552042
filePicker.type = '';
filePicker.type = 'file';
filePicker.value = '';
}
toggleLdapPassword() {
this.showLdapPassword = !this.showLdapPassword;
document.getElementById('password').focus();
}
toggleAzureKey() {
this.showAzureKey = !this.showAzureKey;
document.getElementById('secretKey').focus();
}
toggleOktaKey() {
this.showOktaKey = !this.showOktaKey;
document.getElementById('oktaToken').focus();
}
toggleOneLoginSecret() {
this.showOneLoginSecret = !this.showOneLoginSecret;
document.getElementById('oneLoginClientSecret').focus();
}
toggleOneLoginSecret() {
this.showOneLoginSecret = !this.showOneLoginSecret;
document.getElementById("oneLoginClientSecret").focus();
}
}

View File

@@ -1,23 +1,23 @@
<div class="container-fluid">
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<a class="nav-link" routerLink="dashboard" routerLinkActive="active">
<i class="fa fa-dashboard"></i>
{{'dashboard' | i18n}}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="fa fa-cogs"></i>
{{'settings' | i18n}}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="more" routerLinkActive="active">
<i class="fa fa-sliders"></i>
{{'more' | i18n}}
</a>
</li>
</ul>
<router-outlet></router-outlet>
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<a class="nav-link" routerLink="dashboard" routerLinkActive="active">
<i class="bwi bwi-dashboard"></i>
{{ "dashboard" | i18n }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="bwi bwi-cogs"></i>
{{ "settings" | i18n }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="more" routerLinkActive="active">
<i class="bwi bwi-sliders"></i>
{{ "more" | i18n }}
</a>
</li>
</ul>
<router-outlet></router-outlet>
</div>

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { Component } from "@angular/core";
@Component({
selector: 'app-tabs',
templateUrl: 'tabs.component.html',
selector: "app-tabs",
templateUrl: "tabs.component.html",
})
export class TabsComponent { }
export class TabsComponent {}

View File

@@ -1,146 +1,295 @@
import * as fs from 'fs';
import * as path from 'path';
import * as fs from "fs";
import * as path from "path";
import { LogLevelType } from 'jslib-common/enums/logLevelType';
import { StorageService as StorageServiceAbstraction } from "jslib-common/abstractions/storage.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "jslib-common/abstractions/twoFactor.service";
import { ClientType } from "jslib-common/enums/clientType";
import { LogLevelType } from "jslib-common/enums/logLevelType";
import { StateFactory } from "jslib-common/factories/stateFactory";
import { GlobalState } from "jslib-common/models/domain/globalState";
import { ApiLogInCredentials } from "jslib-common/models/domain/logInCredentials";
import { AppIdService } from "jslib-common/services/appId.service";
import { CipherService } from "jslib-common/services/cipher.service";
import { CollectionService } from "jslib-common/services/collection.service";
import { ContainerService } from "jslib-common/services/container.service";
import { CryptoService } from "jslib-common/services/crypto.service";
import { EnvironmentService } from "jslib-common/services/environment.service";
import { FileUploadService } from "jslib-common/services/fileUpload.service";
import { FolderService } from "jslib-common/services/folder.service";
import { KeyConnectorService } from "jslib-common/services/keyConnector.service";
import { NoopMessagingService } from "jslib-common/services/noopMessaging.service";
import { OrganizationService } from "jslib-common/services/organization.service";
import { PasswordGenerationService } from "jslib-common/services/passwordGeneration.service";
import { PolicyService } from "jslib-common/services/policy.service";
import { ProviderService } from "jslib-common/services/provider.service";
import { SearchService } from "jslib-common/services/search.service";
import { SendService } from "jslib-common/services/send.service";
import { SettingsService } from "jslib-common/services/settings.service";
import { TokenService } from "jslib-common/services/token.service";
import { CliPlatformUtilsService } from "jslib-node/cli/services/cliPlatformUtils.service";
import { ConsoleLogService } from "jslib-node/cli/services/consoleLog.service";
import { NodeApiService } from "jslib-node/services/nodeApi.service";
import { NodeCryptoFunctionService } from "jslib-node/services/nodeCryptoFunction.service";
import { AuthService } from './services/auth.service';
import { Account } from "./models/account";
import { Program } from "./program";
import { AuthService } from "./services/auth.service";
import { I18nService } from "./services/i18n.service";
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
import { LowdbStorageService } from "./services/lowdbStorage.service";
import { NoopTwoFactorService } from "./services/noop/noopTwoFactor.service";
import { StateService } from "./services/state.service";
import { StateMigrationService } from "./services/stateMigration.service";
import { SyncService } from "./services/sync.service";
import { ConfigurationService } from './services/configuration.service';
import { I18nService } from './services/i18n.service';
import { KeytarSecureStorageService } from './services/keytarSecureStorage.service';
import { LowdbStorageService } from './services/lowdbStorage.service';
import { NodeApiService } from './services/nodeApi.service';
import { SyncService } from './services/sync.service';
import { CliPlatformUtilsService } from 'jslib-node/cli/services/cliPlatformUtils.service';
import { ConsoleLogService } from 'jslib-node/cli/services/consoleLog.service';
import { NodeCryptoFunctionService } from 'jslib-node/services/nodeCryptoFunction.service';
import { ApiKeyService } from 'jslib-common/services/apiKey.service';
import { AppIdService } from 'jslib-common/services/appId.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
import { ContainerService } from 'jslib-common/services/container.service';
import { CryptoService } from 'jslib-common/services/crypto.service';
import { EnvironmentService } from 'jslib-common/services/environment.service';
import { NoopMessagingService } from 'jslib-common/services/noopMessaging.service';
import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service';
import { TokenService } from 'jslib-common/services/token.service';
import { UserService } from 'jslib-common/services/user.service';
import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service';
import { Program } from './program';
import { refreshToken } from './services/api.service';
// tslint:disable-next-line
const packageJson = require('./package.json');
// eslint-disable-next-line
const packageJson = require("./package.json");
export const searchService: SearchService = null;
export class Main {
dataFilePath: string;
logService: ConsoleLogService;
messagingService: NoopMessagingService;
storageService: LowdbStorageService;
secureStorageService: StorageServiceAbstraction;
i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService;
constantsService: ConstantsService;
cryptoService: CryptoService;
tokenService: TokenService;
appIdService: AppIdService;
apiService: NodeApiService;
environmentService: EnvironmentService;
apiKeyService: ApiKeyService;
userService: UserService;
containerService: ContainerService;
cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService;
configurationService: ConfigurationService;
syncService: SyncService;
passwordGenerationService: PasswordGenerationService;
program: Program;
dataFilePath: string;
logService: ConsoleLogService;
messagingService: NoopMessagingService;
storageService: LowdbStorageService;
secureStorageService: StorageServiceAbstraction;
i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService;
cryptoService: CryptoService;
tokenService: TokenService;
appIdService: AppIdService;
apiService: NodeApiService;
environmentService: EnvironmentService;
containerService: ContainerService;
cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService;
collectionService: CollectionService;
cipherService: CipherService;
fileUploadService: FileUploadService;
folderService: FolderService;
searchService: SearchService;
sendService: SendService;
settingsService: SettingsService;
syncService: SyncService;
passwordGenerationService: PasswordGenerationService;
policyService: PolicyService;
keyConnectorService: KeyConnectorService;
program: Program;
stateService: StateService;
stateMigrationService: StateMigrationService;
organizationService: OrganizationService;
providerService: ProviderService;
twoFactorService: TwoFactorServiceAbstraction;
constructor() {
const applicationName = 'Bitwarden Directory Connector';
if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) {
this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
} else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) {
this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR);
} else if (fs.existsSync(path.join(__dirname, 'bitwarden-connector-appdata'))) {
this.dataFilePath = path.join(__dirname, 'bitwarden-connector-appdata');
} else if (process.platform === 'darwin') {
this.dataFilePath = path.join(process.env.HOME, 'Library/Application Support/' + applicationName);
} else if (process.platform === 'win32') {
this.dataFilePath = path.join(process.env.APPDATA, applicationName);
} else if (process.env.XDG_CONFIG_HOME) {
this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName);
} else {
this.dataFilePath = path.join(process.env.HOME, '.config/' + applicationName);
}
const plaintextSecrets = process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS === 'true';
this.i18nService = new I18nService('en', './locales');
this.platformUtilsService = new CliPlatformUtilsService('connector', packageJson);
this.logService = new ConsoleLogService(this.platformUtilsService.isDev(),
level => process.env.BITWARDENCLI_CONNECTOR_DEBUG !== 'true' && level <= LogLevelType.Info);
this.cryptoFunctionService = new NodeCryptoFunctionService();
this.storageService = new LowdbStorageService(this.logService, null, this.dataFilePath, false, true);
this.secureStorageService = plaintextSecrets ?
this.storageService : new KeytarSecureStorageService(applicationName);
this.cryptoService = new CryptoService(this.storageService, this.secureStorageService,
this.cryptoFunctionService, this.platformUtilsService, this.logService);
this.appIdService = new AppIdService(this.storageService);
this.tokenService = new TokenService(this.storageService);
this.messagingService = new NoopMessagingService();
this.environmentService = new EnvironmentService(this.storageService);
this.apiService = new NodeApiService(this.tokenService, this.platformUtilsService, this.environmentService,
() => refreshToken(this.apiKeyService, this.authService), async (expired: boolean) => await this.logout(),
'Bitwarden_DC/' + this.platformUtilsService.getApplicationVersion() +
' (' + this.platformUtilsService.getDeviceString().toUpperCase() + ')', (clientId, clientSecret) =>
this.authService.logInApiKey(clientId, clientSecret));
this.apiKeyService = new ApiKeyService(this.tokenService, this.storageService);
this.userService = new UserService(this.tokenService, this.storageService);
this.containerService = new ContainerService(this.cryptoService);
this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, this.tokenService,
this.appIdService, this.i18nService, this.platformUtilsService, this.messagingService, null,
this.logService, this.apiKeyService, false);
this.configurationService = new ConfigurationService(this.storageService, this.secureStorageService,
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== 'true');
this.syncService = new SyncService(this.configurationService, this.logService, this.cryptoFunctionService,
this.apiService, this.messagingService, this.i18nService, this.environmentService);
this.passwordGenerationService = new PasswordGenerationService(this.cryptoService, this.storageService, null);
this.program = new Program(this);
constructor() {
const applicationName = "Bitwarden Directory Connector";
if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) {
this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
} else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) {
this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR);
} else if (fs.existsSync(path.join(__dirname, "bitwarden-connector-appdata"))) {
this.dataFilePath = path.join(__dirname, "bitwarden-connector-appdata");
} else if (process.platform === "darwin") {
this.dataFilePath = path.join(
process.env.HOME,
"Library/Application Support/" + applicationName
);
} else if (process.platform === "win32") {
this.dataFilePath = path.join(process.env.APPDATA, applicationName);
} else if (process.env.XDG_CONFIG_HOME) {
this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName);
} else {
this.dataFilePath = path.join(process.env.HOME, ".config/" + applicationName);
}
async run() {
await this.init();
this.program.run();
}
const plaintextSecrets = process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS === "true";
this.i18nService = new I18nService("en", "./locales");
this.platformUtilsService = new CliPlatformUtilsService(
ClientType.DirectoryConnector,
packageJson
);
this.logService = new ConsoleLogService(
this.platformUtilsService.isDev(),
(level) => process.env.BITWARDENCLI_CONNECTOR_DEBUG !== "true" && level <= LogLevelType.Info
);
this.cryptoFunctionService = new NodeCryptoFunctionService();
this.storageService = new LowdbStorageService(
this.logService,
null,
this.dataFilePath,
false,
true
);
this.secureStorageService = plaintextSecrets
? this.storageService
: new KeytarSecureStorageService(applicationName);
async logout() {
await this.tokenService.clearToken();
await this.apiKeyService.clear();
}
this.stateMigrationService = new StateMigrationService(
this.storageService,
this.secureStorageService,
new StateFactory(GlobalState, Account)
);
private async init() {
await this.storageService.init();
this.containerService.attachToWindow(global);
await this.environmentService.setUrlsFromStorage();
// Dev Server URLs. Comment out the line above.
// this.apiService.setUrls({
// base: null,
// api: 'http://localhost:4000',
// identity: 'http://localhost:33656',
// });
const locale = await this.storageService.get<string>(ConstantsService.localeKey);
await this.i18nService.init(locale);
this.authService.init();
this.stateService = new StateService(
this.storageService,
this.secureStorageService,
this.logService,
this.stateMigrationService,
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== "true",
new StateFactory(GlobalState, Account)
);
const installedVersion = await this.storageService.get<string>(ConstantsService.installedVersionKey);
const currentVersion = await this.platformUtilsService.getApplicationVersion();
if (installedVersion == null || installedVersion !== currentVersion) {
await this.storageService.save(ConstantsService.installedVersionKey, currentVersion);
}
this.cryptoService = new CryptoService(
this.cryptoFunctionService,
this.platformUtilsService,
this.logService,
this.stateService
);
this.appIdService = new AppIdService(this.storageService);
this.tokenService = new TokenService(this.stateService);
this.messagingService = new NoopMessagingService();
this.environmentService = new EnvironmentService(this.stateService);
this.apiService = new NodeApiService(
this.tokenService,
this.platformUtilsService,
this.environmentService,
async (expired: boolean) => await this.logout(),
"Bitwarden_DC/" +
this.platformUtilsService.getApplicationVersion() +
" (" +
this.platformUtilsService.getDeviceString().toUpperCase() +
")",
(clientId, clientSecret) =>
this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret))
);
this.containerService = new ContainerService(this.cryptoService);
this.organizationService = new OrganizationService(this.stateService);
this.keyConnectorService = new KeyConnectorService(
this.stateService,
this.cryptoService,
this.apiService,
this.tokenService,
this.logService,
this.organizationService,
this.cryptoFunctionService
);
this.twoFactorService = new NoopTwoFactorService();
this.authService = new AuthService(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.keyConnectorService,
this.environmentService,
this.stateService,
this.twoFactorService,
this.i18nService
);
this.syncService = new SyncService(
this.logService,
this.cryptoFunctionService,
this.apiService,
this.messagingService,
this.i18nService,
this.environmentService,
this.stateService
);
this.policyService = new PolicyService(
this.stateService,
this.organizationService,
this.apiService
);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
this.policyService,
this.stateService
);
this.settingsService = new SettingsService(this.stateService);
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
this.cipherService = new CipherService(
this.cryptoService,
this.settingsService,
this.apiService,
this.fileUploadService,
this.i18nService,
() => searchService,
this.logService,
this.stateService
);
this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService);
this.folderService = new FolderService(
this.cryptoService,
this.apiService,
this.i18nService,
this.cipherService,
this.stateService
);
this.collectionService = new CollectionService(
this.cryptoService,
this.i18nService,
this.stateService
);
this.sendService = new SendService(
this.cryptoService,
this.apiService,
this.fileUploadService,
this.i18nService,
this.cryptoFunctionService,
this.stateService
);
this.providerService = new ProviderService(this.stateService);
this.program = new Program(this);
}
async run() {
await this.init();
this.program.run();
}
async logout() {
await this.tokenService.clearToken();
await this.stateService.clean();
}
private async init() {
await this.storageService.init();
await this.stateService.init();
this.containerService.attachToWindow(global);
await this.environmentService.setUrlsFromStorage();
// Dev Server URLs. Comment out the line above.
// this.apiService.setUrls({
// base: null,
// api: 'http://localhost:4000',
// identity: 'http://localhost:33656',
// });
const locale = await this.stateService.getLocale();
await this.i18nService.init(locale);
const installedVersion = await this.stateService.getInstalledVersion();
const currentVersion = await this.platformUtilsService.getApplicationVersion();
if (installedVersion == null || installedVersion !== currentVersion) {
await this.stateService.setInstalledVersion(currentVersion);
}
}
}
const main = new Main();

View File

@@ -1,22 +1,21 @@
import * as program from 'commander';
import * as program from "commander";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { ConfigurationService } from '../services/configuration.service';
import { Response } from 'jslib-node/cli/models/response';
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse';
import { StateService } from "../abstractions/state.service";
export class ClearCacheCommand {
constructor(private configurationService: ConfigurationService, private i18nService: I18nService) { }
constructor(private i18nService: I18nService, private stateService: StateService) {}
async run(cmd: program.OptionValues): Promise<Response> {
try {
await this.configurationService.clearStatefulSettings(true);
const res = new MessageResponse(this.i18nService.t('syncCacheCleared'), null);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
async run(cmd: program.OptionValues): Promise<Response> {
try {
await this.stateService.clearSyncSettings(true);
const res = new MessageResponse(this.i18nService.t("syncCacheCleared"), null);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}

View File

@@ -1,150 +1,152 @@
import * as program from 'commander';
import * as program from "commander";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { ConfigurationService } from '../services/configuration.service';
import { DirectoryType } from '../enums/directoryType';
import { Response } from 'jslib-node/cli/models/response';
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse';
import { AzureConfiguration } from '../models/azureConfiguration';
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
import { LdapConfiguration } from '../models/ldapConfiguration';
import { OktaConfiguration } from '../models/oktaConfiguration';
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { ConnectorUtils } from '../utils';
import { NodeUtils } from 'jslib-common/misc/nodeUtils';
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { AzureConfiguration } from "../models/azureConfiguration";
import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
import { LdapConfiguration } from "../models/ldapConfiguration";
import { OktaConfiguration } from "../models/oktaConfiguration";
import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { ConnectorUtils } from "../utils";
export class ConfigCommand {
private directory: DirectoryType;
private ldap = new LdapConfiguration();
private gsuite = new GSuiteConfiguration();
private azure = new AzureConfiguration();
private okta = new OktaConfiguration();
private oneLogin = new OneLoginConfiguration();
private sync = new SyncConfiguration();
private directory: DirectoryType;
private ldap = new LdapConfiguration();
private gsuite = new GSuiteConfiguration();
private azure = new AzureConfiguration();
private okta = new OktaConfiguration();
private oneLogin = new OneLoginConfiguration();
private sync = new SyncConfiguration();
constructor(private environmentService: EnvironmentService, private i18nService: I18nService,
private configurationService: ConfigurationService) { }
constructor(
private environmentService: EnvironmentService,
private i18nService: I18nService,
private stateService: StateService
) {}
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
setting = setting.toLowerCase();
if (value == null || value === '') {
if (options.secretfile) {
value = await NodeUtils.readFirstLine(options.secretfile);
} else if (options.secretenv && process.env[options.secretenv]) {
value = process.env[options.secretenv];
}
}
try {
switch (setting) {
case 'server':
await this.setServer(value);
break;
case 'directory':
await this.setDirectory(value);
break;
case 'ldap.password':
await this.setLdapPassword(value);
break;
case 'gsuite.key':
await this.setGSuiteKey(value);
break;
case 'azure.key':
await this.setAzureKey(value);
break;
case 'okta.token':
await this.setOktaToken(value);
break;
case 'onelogin.secret':
await this.setOneLoginSecret(value);
break;
default:
return Response.badRequest('Unknown setting.');
}
} catch (e) {
return Response.error(e);
}
const res = new MessageResponse(this.i18nService.t('savedSetting', setting), null);
return Response.success(res);
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
setting = setting.toLowerCase();
if (value == null || value === "") {
if (options.secretfile) {
value = await NodeUtils.readFirstLine(options.secretfile);
} else if (options.secretenv && process.env[options.secretenv]) {
value = process.env[options.secretenv];
}
}
private async setServer(url: string) {
url = (url === 'null' || url === 'bitwarden.com' || url === 'https://bitwarden.com' ? null : url);
await this.environmentService.setUrls({
base: url,
});
try {
switch (setting) {
case "server":
await this.setServer(value);
break;
case "directory":
await this.setDirectory(value);
break;
case "ldap.password":
await this.setLdapPassword(value);
break;
case "gsuite.key":
await this.setGSuiteKey(value);
break;
case "azure.key":
await this.setAzureKey(value);
break;
case "okta.token":
await this.setOktaToken(value);
break;
case "onelogin.secret":
await this.setOneLoginSecret(value);
break;
default:
return Response.badRequest("Unknown setting.");
}
} catch (e) {
return Response.error(e);
}
const res = new MessageResponse(this.i18nService.t("savedSetting", setting), null);
return Response.success(res);
}
private async setDirectory(type: string) {
const dir = parseInt(type, null);
if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) {
throw new Error('Invalid directory type value.');
}
await this.loadConfig();
this.directory = dir;
await this.saveConfig();
}
private async setServer(url: string) {
url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url;
await this.environmentService.setUrls({
base: url,
});
}
private async setLdapPassword(password: string) {
await this.loadConfig();
this.ldap.password = password;
await this.saveConfig();
private async setDirectory(type: string) {
const dir = parseInt(type, null);
if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) {
throw new Error("Invalid directory type value.");
}
await this.loadConfig();
this.directory = dir;
await this.saveConfig();
}
private async setGSuiteKey(key: string) {
await this.loadConfig();
this.gsuite.privateKey = key != null ? key.trimLeft() : null;
await this.saveConfig();
}
private async setLdapPassword(password: string) {
await this.loadConfig();
this.ldap.password = password;
await this.saveConfig();
}
private async setAzureKey(key: string) {
await this.loadConfig();
this.azure.key = key;
await this.saveConfig();
}
private async setGSuiteKey(key: string) {
await this.loadConfig();
this.gsuite.privateKey = key != null ? key.trimLeft() : null;
await this.saveConfig();
}
private async setOktaToken(token: string) {
await this.loadConfig();
this.okta.token = token;
await this.saveConfig();
}
private async setAzureKey(key: string) {
await this.loadConfig();
this.azure.key = key;
await this.saveConfig();
}
private async setOneLoginSecret(secret: string) {
await this.loadConfig();
this.oneLogin.clientSecret = secret;
await this.saveConfig();
}
private async setOktaToken(token: string) {
await this.loadConfig();
this.okta.token = token;
await this.saveConfig();
}
private async loadConfig() {
this.directory = await this.configurationService.getDirectoryType();
this.ldap = (await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) ||
this.ldap;
this.gsuite = (await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
this.gsuite;
this.azure = (await this.configurationService.getDirectory<AzureConfiguration>(
DirectoryType.AzureActiveDirectory)) || this.azure;
this.okta = (await this.configurationService.getDirectory<OktaConfiguration>(
DirectoryType.Okta)) || this.okta;
this.oneLogin = (await this.configurationService.getDirectory<OneLoginConfiguration>(
DirectoryType.OneLogin)) || this.oneLogin;
this.sync = (await this.configurationService.getSync()) || this.sync;
}
private async setOneLoginSecret(secret: string) {
await this.loadConfig();
this.oneLogin.clientSecret = secret;
await this.saveConfig();
}
private async saveConfig() {
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
await this.configurationService.saveDirectoryType(this.directory);
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap);
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite);
await this.configurationService.saveDirectory(DirectoryType.AzureActiveDirectory, this.azure);
await this.configurationService.saveDirectory(DirectoryType.Okta, this.okta);
await this.configurationService.saveDirectory(DirectoryType.OneLogin, this.oneLogin);
await this.configurationService.saveSync(this.sync);
}
private async loadConfig() {
this.directory = await this.stateService.getDirectoryType();
this.ldap =
(await this.stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) || this.ldap;
this.gsuite =
(await this.stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
this.gsuite;
this.azure =
(await this.stateService.getDirectory<AzureConfiguration>(
DirectoryType.AzureActiveDirectory
)) || this.azure;
this.okta =
(await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) || this.okta;
this.oneLogin =
(await this.stateService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin)) ||
this.oneLogin;
this.sync = (await this.stateService.getSync()) || this.sync;
}
private async saveConfig() {
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
await this.stateService.setDirectoryType(this.directory);
await this.stateService.setDirectory(DirectoryType.Ldap, this.ldap);
await this.stateService.setDirectory(DirectoryType.GSuite, this.gsuite);
await this.stateService.setDirectory(DirectoryType.AzureActiveDirectory, this.azure);
await this.stateService.setDirectory(DirectoryType.Okta, this.okta);
await this.stateService.setDirectory(DirectoryType.OneLogin, this.oneLogin);
await this.stateService.setSync(this.sync);
}
}

View File

@@ -1,29 +1,31 @@
import * as program from 'commander';
import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { ConfigurationService } from '../services/configuration.service';
import { Response } from 'jslib-node/cli/models/response';
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse';
import { StateService } from "../abstractions/state.service";
export class LastSyncCommand {
constructor(private configurationService: ConfigurationService) { }
constructor(private stateService: StateService) {}
async run(object: string): Promise<Response> {
try {
switch (object.toLowerCase()) {
case 'groups':
const groupsDate = await this.configurationService.getLastGroupSyncDate();
const groupsRes = new StringResponse(groupsDate == null ? null : groupsDate.toISOString());
return Response.success(groupsRes);
case 'users':
const usersDate = await this.configurationService.getLastUserSyncDate();
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
return Response.success(usersRes);
default:
return Response.badRequest('Unknown object.');
}
} catch (e) {
return Response.error(e);
async run(object: string): Promise<Response> {
try {
switch (object.toLowerCase()) {
case "groups": {
const groupsDate = await this.stateService.getLastGroupSync();
const groupsRes = new StringResponse(
groupsDate == null ? null : groupsDate.toISOString()
);
return Response.success(groupsRes);
}
case "users": {
const usersDate = await this.stateService.getLastUserSync();
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
return Response.success(usersRes);
}
default:
return Response.badRequest("Unknown object.");
}
} catch (e) {
return Response.error(e);
}
}
}

View File

@@ -1,23 +1,24 @@
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { Response } from "jslib-node/cli/models/response";
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
import { SyncService } from '../services/sync.service';
import { Response } from 'jslib-node/cli/models/response';
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse';
import { SyncService } from "../services/sync.service";
export class SyncCommand {
constructor(private syncService: SyncService, private i18nService: I18nService) { }
constructor(private syncService: SyncService, private i18nService: I18nService) {}
async run(): Promise<Response> {
try {
const result = await this.syncService.sync(false, false);
const groupCount = result[0] != null ? result[0].length : 0;
const userCount = result[1] != null ? result[1].length : 0;
const res = new MessageResponse(this.i18nService.t('syncingComplete'),
this.i18nService.t('syncCounts', groupCount.toString(), userCount.toString()));
return Response.success(res);
} catch (e) {
return Response.error(e);
}
async run(): Promise<Response> {
try {
const result = await this.syncService.sync(false, false);
const groupCount = result[0] != null ? result[0].length : 0;
const userCount = result[1] != null ? result[1].length : 0;
const res = new MessageResponse(
this.i18nService.t("syncingComplete"),
this.i18nService.t("syncCounts", groupCount.toString(), userCount.toString())
);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}

View File

@@ -1,24 +1,26 @@
import * as program from 'commander';
import * as program from "commander";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { Response } from "jslib-node/cli/models/response";
import { SyncService } from '../services/sync.service';
import { ConnectorUtils } from '../utils';
import { Response } from 'jslib-node/cli/models/response';
import { TestResponse } from '../models/response/testResponse';
import { TestResponse } from "../models/response/testResponse";
import { SyncService } from "../services/sync.service";
import { ConnectorUtils } from "../utils";
export class TestCommand {
constructor(private syncService: SyncService, private i18nService: I18nService) { }
constructor(private syncService: SyncService, private i18nService: I18nService) {}
async run(cmd: program.OptionValues): Promise<Response> {
try {
const result = await ConnectorUtils.simulate(this.syncService, this.i18nService, cmd.last || false);
const res = new TestResponse(result);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
async run(cmd: program.OptionValues): Promise<Response> {
try {
const result = await ConnectorUtils.simulate(
this.syncService,
this.i18nService,
cmd.last || false
);
const res = new TestResponse(result);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
}

View File

@@ -1,7 +1,7 @@
export enum DirectoryType {
Ldap = 0,
AzureActiveDirectory = 1,
GSuite = 2,
Okta = 3,
OneLogin = 4,
Ldap = 0,
AzureActiveDirectory = 1,
GSuite = 2,
Okta = 3,
OneLogin = 4,
}

2
src/global.d.ts vendored
View File

@@ -1,3 +1,3 @@
declare function escape(s: string): string;
declare function unescape(s: string): string;
declare module 'duo_web_sdk';
declare module "duo_web_sdk";

View File

@@ -1,16 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline';
img-src 'self' data: *; child-src *; frame-src *; connect-src *;">
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'self' 'unsafe-inline';
img-src 'self' data: *; child-src *; frame-src *; connect-src *;"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bitwarden Directory Connector</title>
<base href="">
</head>
<body>
<base href="" />
</head>
<body>
<app-root>
<div id="loading"><i class="fa fa-spinner fa-spin fa-3x"></i></div>
<div id="loading"><i class="bwi bwi-spinner bwi-spin bwi-3x"></i></div>
</app-root>
</body>
</body>
</html>

View File

@@ -354,6 +354,9 @@
"rootPath": {
"message": "Root Path"
},
"identityAuthority": {
"message": "Identity Authority"
},
"tenant": {
"message": "Tenant"
},

View File

@@ -1,109 +1,152 @@
import { app } from 'electron';
import * as path from 'path';
import * as path from "path";
import { MenuMain } from './main/menu.main';
import { MessagingMain } from './main/messaging.main';
import { I18nService } from './services/i18n.service';
import { app } from "electron";
import { KeytarStorageListener } from 'jslib-electron/keytarStorageListener';
import { ElectronLogService } from 'jslib-electron/services/electronLog.service';
import { ElectronMainMessagingService } from 'jslib-electron/services/electronMainMessaging.service';
import { ElectronStorageService } from 'jslib-electron/services/electronStorage.service';
import { TrayMain } from 'jslib-electron/tray.main';
import { UpdaterMain } from 'jslib-electron/updater.main';
import { WindowMain } from 'jslib-electron/window.main';
import { StateFactory } from "jslib-common/factories/stateFactory";
import { GlobalState } from "jslib-common/models/domain/globalState";
import { KeytarStorageListener } from "jslib-electron/keytarStorageListener";
import { ElectronLogService } from "jslib-electron/services/electronLog.service";
import { ElectronMainMessagingService } from "jslib-electron/services/electronMainMessaging.service";
import { ElectronStorageService } from "jslib-electron/services/electronStorage.service";
import { TrayMain } from "jslib-electron/tray.main";
import { UpdaterMain } from "jslib-electron/updater.main";
import { WindowMain } from "jslib-electron/window.main";
import { MenuMain } from "./main/menu.main";
import { MessagingMain } from "./main/messaging.main";
import { Account } from "./models/account";
import { I18nService } from "./services/i18n.service";
import { StateService } from "./services/state.service";
export class Main {
logService: ElectronLogService;
i18nService: I18nService;
storageService: ElectronStorageService;
messagingService: ElectronMainMessagingService;
keytarStorageListener: KeytarStorageListener;
logService: ElectronLogService;
i18nService: I18nService;
storageService: ElectronStorageService;
messagingService: ElectronMainMessagingService;
keytarStorageListener: KeytarStorageListener;
stateService: StateService;
windowMain: WindowMain;
messagingMain: MessagingMain;
menuMain: MenuMain;
updaterMain: UpdaterMain;
trayMain: TrayMain;
windowMain: WindowMain;
messagingMain: MessagingMain;
menuMain: MenuMain;
updaterMain: UpdaterMain;
trayMain: TrayMain;
constructor() {
// Set paths for portable builds
let appDataPath = null;
if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR != null) {
appDataPath = process.env.BITWARDEN_CONNECTOR_APPDATA_DIR;
} else if (process.platform === 'win32' && process.env.PORTABLE_EXECUTABLE_DIR != null) {
appDataPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR, 'bitwarden-connector-appdata');
}
if (appDataPath != null) {
app.setPath('userData', appDataPath);
}
app.setPath('logs', path.join(app.getPath('userData'), 'logs'));
const args = process.argv.slice(1);
const watch = args.some(val => val === '--watch');
if (watch) {
// tslint:disable-next-line
require('electron-reload')(__dirname, {});
}
this.logService = new ElectronLogService(null, app.getPath('userData'));
this.i18nService = new I18nService('en', './locales/');
this.storageService = new ElectronStorageService(app.getPath('userData'));
this.windowMain = new WindowMain(this.storageService, false, 800, 600, arg => this.processDeepLink(arg), null);
this.menuMain = new MenuMain(this);
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'directory-connector', () => {
this.messagingService.send('checkingForUpdate');
}, () => {
this.messagingService.send('doneCheckingForUpdate');
}, () => {
this.messagingService.send('doneCheckingForUpdate');
}, 'bitwardenDirectoryConnector');
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.storageService);
this.messagingMain = new MessagingMain(this.windowMain, this.menuMain, this.updaterMain, this.trayMain);
this.messagingService = new ElectronMainMessagingService(this.windowMain, message => {
this.messagingMain.onMessage(message);
});
this.keytarStorageListener = new KeytarStorageListener('Bitwarden Directory Connector', null);
constructor() {
// Set paths for portable builds
let appDataPath = null;
if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR != null) {
appDataPath = process.env.BITWARDEN_CONNECTOR_APPDATA_DIR;
} else if (process.platform === "win32" && process.env.PORTABLE_EXECUTABLE_DIR != null) {
appDataPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR, "bitwarden-connector-appdata");
}
bootstrap() {
this.keytarStorageListener.init();
this.windowMain.init().then(async () => {
await this.i18nService.init(app.getLocale());
this.menuMain.init();
this.messagingMain.init();
await this.updaterMain.init();
await this.trayMain.init(this.i18nService.t('bitwardenDirectoryConnector'));
if (appDataPath != null) {
app.setPath("userData", appDataPath);
}
app.setPath("logs", path.join(app.getPath("userData"), "logs"));
if (!app.isDefaultProtocolClient('bwdc')) {
app.setAsDefaultProtocolClient('bwdc');
}
const args = process.argv.slice(1);
const watch = args.some((val) => val === "--watch");
// Process protocol for macOS
app.on('open-url', (event, url) => {
event.preventDefault();
this.processDeepLink([url]);
});
}, (e: any) => {
// tslint:disable-next-line
console.error(e);
});
if (watch) {
// eslint-disable-next-line
require("electron-reload")(__dirname, {});
}
private processDeepLink(argv: string[]): void {
argv.filter(s => s.indexOf('bwdc://') === 0).forEach(s => {
const url = new URL(s);
const code = url.searchParams.get('code');
const receivedState = url.searchParams.get('state');
if (code != null && receivedState != null) {
this.messagingService.send('ssoCallback', { code: code, state: receivedState });
}
this.logService = new ElectronLogService(null, app.getPath("userData"));
this.i18nService = new I18nService("en", "./locales/");
this.storageService = new ElectronStorageService(app.getPath("userData"));
this.stateService = new StateService(
this.storageService,
null,
this.logService,
null,
true,
new StateFactory(GlobalState, Account)
);
this.windowMain = new WindowMain(
this.stateService,
this.logService,
false,
800,
600,
(arg) => this.processDeepLink(arg),
null
);
this.menuMain = new MenuMain(this);
this.updaterMain = new UpdaterMain(
this.i18nService,
this.windowMain,
"directory-connector",
() => {
this.messagingService.send("checkingForUpdate");
},
() => {
this.messagingService.send("doneCheckingForUpdate");
},
() => {
this.messagingService.send("doneCheckingForUpdate");
},
"bitwardenDirectoryConnector"
);
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.stateService);
this.messagingMain = new MessagingMain(
this.windowMain,
this.menuMain,
this.updaterMain,
this.trayMain
);
this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => {
this.messagingMain.onMessage(message);
});
this.keytarStorageListener = new KeytarStorageListener("Bitwarden Directory Connector", null);
}
bootstrap() {
this.keytarStorageListener.init();
this.windowMain.init().then(
async () => {
await this.i18nService.init(app.getLocale());
this.menuMain.init();
this.messagingMain.init();
await this.updaterMain.init();
await this.trayMain.init(this.i18nService.t("bitwardenDirectoryConnector"));
if (!app.isDefaultProtocolClient("bwdc")) {
app.setAsDefaultProtocolClient("bwdc");
}
// Process protocol for macOS
app.on("open-url", (event, url) => {
event.preventDefault();
this.processDeepLink([url]);
});
}
},
(e: any) => {
// eslint-disable-next-line
console.error(e);
}
);
}
private processDeepLink(argv: string[]): void {
argv
.filter((s) => s.indexOf("bwdc://") === 0)
.forEach((s) => {
const url = new URL(s);
const code = url.searchParams.get("code");
const receivedState = url.searchParams.get("state");
if (code != null && receivedState != null) {
this.messagingService.send("ssoCallback", { code: code, state: receivedState });
}
});
}
}
const main = new Main();

View File

@@ -1,68 +1,69 @@
import {
Menu,
MenuItem,
MenuItemConstructorOptions,
} from 'electron';
import { Menu, MenuItemConstructorOptions } from "electron";
import { Main } from '../main';
import { BaseMenu } from "jslib-electron/baseMenu";
import { BaseMenu } from 'jslib-electron/baseMenu';
import { Main } from "../main";
export class MenuMain extends BaseMenu {
menu: Menu;
menu: Menu;
constructor(private main: Main) {
super(main.i18nService, main.windowMain);
constructor(private main: Main) {
super(main.i18nService, main.windowMain);
}
init() {
this.initProperties();
this.initContextMenu();
this.initApplicationMenu();
}
private initApplicationMenu() {
const template: MenuItemConstructorOptions[] = [
this.editMenuItemOptions,
{
label: this.i18nService.t("view"),
submenu: this.viewSubMenuItemOptions,
},
this.windowMenuItemOptions,
];
if (process.platform === "darwin") {
const firstMenuPart: MenuItemConstructorOptions[] = [
{
label: this.i18nService.t("aboutBitwarden"),
role: "about",
},
];
template.unshift({
label: this.main.i18nService.t("bitwardenDirectoryConnector"),
submenu: firstMenuPart.concat(this.macAppMenuItemOptions),
});
// Window menu
template[template.length - 1].submenu = this.macWindowSubmenuOptions;
}
init() {
this.initProperties();
this.initContextMenu();
this.initApplicationMenu();
}
(template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(
1,
0,
{
label: this.main.i18nService.t(
process.platform === "darwin" ? "hideToMenuBar" : "hideToTray"
),
click: () => this.main.messagingService.send("hideToTray"),
accelerator: "CmdOrCtrl+Shift+M",
},
{
type: "checkbox",
label: this.main.i18nService.t("alwaysOnTop"),
checked: this.windowMain.win.isAlwaysOnTop(),
click: () => this.main.windowMain.toggleAlwaysOnTop(),
accelerator: "CmdOrCtrl+Shift+T",
}
);
private initApplicationMenu() {
const template: MenuItemConstructorOptions[] = [
this.editMenuItemOptions,
{
label: this.i18nService.t('view'),
submenu: this.viewSubMenuItemOptions,
},
this.windowMenuItemOptions,
];
if (process.platform === 'darwin') {
const firstMenuPart: MenuItemConstructorOptions[] = [
{
label: this.i18nService.t('aboutBitwarden'),
role: 'about',
},
];
template.unshift({
label: this.main.i18nService.t('bitwardenDirectoryConnector'),
submenu: firstMenuPart.concat(this.macAppMenuItemOptions),
});
// Window menu
template[template.length - 1].submenu = this.macWindowSubmenuOptions;
}
(template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(1, 0,
{
label: this.main.i18nService.t(process.platform === 'darwin' ? 'hideToMenuBar' : 'hideToTray'),
click: () => this.main.messagingService.send('hideToTray'),
accelerator: 'CmdOrCtrl+Shift+M',
},
{
type: 'checkbox',
label: this.main.i18nService.t('alwaysOnTop'),
checked: this.windowMain.win.isAlwaysOnTop(),
click: () => this.main.windowMain.toggleAlwaysOnTop(),
accelerator: 'CmdOrCtrl+Shift+T',
});
this.menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(this.menu);
}
this.menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(this.menu);
}
}

View File

@@ -1,67 +1,68 @@
import {
app,
ipcMain,
} from 'electron';
import { ipcMain } from "electron";
import { TrayMain } from 'jslib-electron/tray.main';
import { UpdaterMain } from 'jslib-electron/updater.main';
import { WindowMain } from 'jslib-electron/window.main';
import { TrayMain } from "jslib-electron/tray.main";
import { UpdaterMain } from "jslib-electron/updater.main";
import { WindowMain } from "jslib-electron/window.main";
import { MenuMain } from './menu.main';
import { MenuMain } from "./menu.main";
const SyncCheckInterval = 60 * 1000; // 1 minute
export class MessagingMain {
private syncTimeout: NodeJS.Timer;
private syncTimeout: NodeJS.Timer;
constructor(private windowMain: WindowMain, private menuMain: MenuMain,
private updaterMain: UpdaterMain, private trayMain: TrayMain) { }
constructor(
private windowMain: WindowMain,
private menuMain: MenuMain,
private updaterMain: UpdaterMain,
private trayMain: TrayMain
) {}
init() {
ipcMain.on('messagingService', async (event: any, message: any) => this.onMessage(message));
}
init() {
ipcMain.on("messagingService", async (event: any, message: any) => this.onMessage(message));
}
onMessage(message: any) {
switch (message.command) {
case 'checkForUpdate':
this.updaterMain.checkForUpdate(true);
break;
case 'scheduleNextDirSync':
this.scheduleNextSync();
break;
case 'cancelDirSync':
this.windowMain.win.webContents.send('messagingService', {
command: 'syncScheduleStopped',
});
if (this.syncTimeout) {
global.clearTimeout(this.syncTimeout);
}
break;
case 'hideToTray':
this.trayMain.hideToTray();
break;
default:
break;
}
}
private scheduleNextSync() {
this.windowMain.win.webContents.send('messagingService', {
command: 'syncScheduleStarted',
onMessage(message: any) {
switch (message.command) {
case "checkForUpdate":
this.updaterMain.checkForUpdate(true);
break;
case "scheduleNextDirSync":
this.scheduleNextSync();
break;
case "cancelDirSync":
this.windowMain.win.webContents.send("messagingService", {
command: "syncScheduleStopped",
});
if (this.syncTimeout) {
global.clearTimeout(this.syncTimeout);
global.clearTimeout(this.syncTimeout);
}
this.syncTimeout = global.setTimeout(() => {
if (this.windowMain.win == null) {
return;
}
this.windowMain.win.webContents.send('messagingService', {
command: 'checkDirSync',
});
}, SyncCheckInterval);
break;
case "hideToTray":
this.trayMain.hideToTray();
break;
default:
break;
}
}
private scheduleNextSync() {
this.windowMain.win.webContents.send("messagingService", {
command: "syncScheduleStarted",
});
if (this.syncTimeout) {
global.clearTimeout(this.syncTimeout);
}
this.syncTimeout = global.setTimeout(() => {
if (this.windowMain.win == null) {
return;
}
this.windowMain.win.webContents.send("messagingService", {
command: "checkDirSync",
});
}, SyncCheckInterval);
}
}

View File

@@ -0,0 +1,62 @@
import { LogInStrategy } from "jslib-common/misc/logInStrategies/logIn.strategy";
import { AccountKeys, AccountProfile, AccountTokens } from "jslib-common/models/domain/account";
import { AuthResult } from "jslib-common/models/domain/authResult";
import { ApiLogInCredentials } from "jslib-common/models/domain/logInCredentials";
import { ApiTokenRequest } from "jslib-common/models/request/identityToken/apiTokenRequest";
import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse";
import { Account, DirectoryConfigurations, DirectorySettings } from "src/models/account";
export class OrganizationLogInStrategy extends LogInStrategy {
tokenRequest: ApiTokenRequest;
async logIn(credentials: ApiLogInCredentials) {
this.tokenRequest = new ApiTokenRequest(
credentials.clientId,
credentials.clientSecret,
await this.buildTwoFactor(),
await this.buildDeviceRequest()
);
return this.startLogIn();
}
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
await this.saveAccountInformation(response);
return new AuthResult();
}
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
const clientId = this.tokenRequest.clientId;
const entityId = clientId.split("organization.")[1];
const clientSecret = this.tokenRequest.clientSecret;
await this.stateService.addAccount(
new Account({
profile: {
...new AccountProfile(),
...{
userId: entityId,
apiKeyClientId: clientId,
entityId: entityId,
},
},
tokens: {
...new AccountTokens(),
...{
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
},
},
keys: {
...new AccountKeys(),
...{
apiKeyClientSecret: clientSecret,
},
},
directorySettings: new DirectorySettings(),
directoryConfigurations: new DirectoryConfigurations(),
})
);
}
}

View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line
export interface IConfiguration {}

47
src/models/account.ts Normal file
View File

@@ -0,0 +1,47 @@
import { Account as BaseAccount } from "jslib-common/models/domain/account";
import { DirectoryType } from "src/enums/directoryType";
import { AzureConfiguration } from "./azureConfiguration";
import { GSuiteConfiguration } from "./gsuiteConfiguration";
import { LdapConfiguration } from "./ldapConfiguration";
import { OktaConfiguration } from "./oktaConfiguration";
import { OneLoginConfiguration } from "./oneLoginConfiguration";
import { SyncConfiguration } from "./syncConfiguration";
export class Account extends BaseAccount {
directoryConfigurations?: DirectoryConfigurations = new DirectoryConfigurations();
directorySettings: DirectorySettings = new DirectorySettings();
clientKeys: ClientKeys = new ClientKeys();
constructor(init: Partial<Account>) {
super(init);
this.directoryConfigurations = init?.directoryConfigurations ?? new DirectoryConfigurations();
this.directorySettings = init?.directorySettings ?? new DirectorySettings();
}
}
export class ClientKeys {
clientId: string;
clientSecret: string;
}
export class DirectoryConfigurations {
ldap: LdapConfiguration;
gsuite: GSuiteConfiguration;
azure: AzureConfiguration;
okta: OktaConfiguration;
oneLogin: OneLoginConfiguration;
}
export class DirectorySettings {
organizationId?: string;
sync?: SyncConfiguration;
directoryType?: DirectoryType;
userDelta?: string;
groupDelta?: string;
lastUserSync?: Date;
lastGroupSync?: Date;
lastSyncHash?: string;
syncingDir?: boolean;
}

View File

@@ -1,5 +1,8 @@
export class AzureConfiguration {
tenant: string;
applicationId: string;
key: string;
import { IConfiguration } from "./IConfiguration";
export class AzureConfiguration implements IConfiguration {
identityAuthority: string;
tenant: string;
applicationId: string;
key: string;
}

View File

@@ -1,8 +1,8 @@
export abstract class Entry {
referenceId: string;
externalId: string;
referenceId: string;
externalId: string;
get displayName(): string {
return this.referenceId;
}
get displayName(): string {
return this.referenceId;
}
}

View File

@@ -1,15 +1,15 @@
import { Entry } from './entry';
import { Entry } from "./entry";
export class GroupEntry extends Entry {
name: string;
userMemberExternalIds = new Set<string>();
groupMemberReferenceIds = new Set<string>();
name: string;
userMemberExternalIds = new Set<string>();
groupMemberReferenceIds = new Set<string>();
get displayName(): string {
if (this.name == null) {
return this.referenceId;
}
return this.name;
get displayName(): string {
if (this.name == null) {
return this.referenceId;
}
return this.name;
}
}

View File

@@ -1,7 +1,9 @@
export class GSuiteConfiguration {
clientEmail: string;
privateKey: string;
domain: string;
adminUser: string;
customer: string;
import { IConfiguration } from "./IConfiguration";
export class GSuiteConfiguration implements IConfiguration {
clientEmail: string;
privateKey: string;
domain: string;
adminUser: string;
customer: string;
}

View File

@@ -1,18 +1,20 @@
export class LdapConfiguration {
ssl = false;
startTls = false;
tlsCaPath: string;
sslAllowUnauthorized = false;
sslCertPath: string;
sslKeyPath: string;
sslCaPath: string;
hostname: string;
port = 389;
domain: string;
rootPath: string;
currentUser = false;
username: string;
password: string;
ad = true;
pagedSearch = true;
import { IConfiguration } from "./IConfiguration";
export class LdapConfiguration implements IConfiguration {
ssl = false;
startTls = false;
tlsCaPath: string;
sslAllowUnauthorized = false;
sslCertPath: string;
sslKeyPath: string;
sslCaPath: string;
hostname: string;
port = 389;
domain: string;
rootPath: string;
currentUser = false;
username: string;
password: string;
ad = true;
pagedSearch = true;
}

View File

@@ -1,4 +1,6 @@
export class OktaConfiguration {
orgUrl: string;
token: string;
import { IConfiguration } from "./IConfiguration";
export class OktaConfiguration implements IConfiguration {
orgUrl: string;
token: string;
}

View File

@@ -1,5 +1,7 @@
export class OneLoginConfiguration {
clientId: string;
clientSecret: string;
region = 'us';
import { IConfiguration } from "./IConfiguration";
export class OneLoginConfiguration implements IConfiguration {
clientId: string;
clientSecret: string;
region = "us";
}

View File

@@ -1,13 +1,13 @@
import { GroupEntry } from '../groupEntry';
import { GroupEntry } from "../groupEntry";
export class GroupResponse {
externalId: string;
displayName: string;
userIds: string[];
externalId: string;
displayName: string;
userIds: string[];
constructor(g: GroupEntry) {
this.externalId = g.externalId;
this.displayName = g.displayName;
this.userIds = Array.from(g.userMemberExternalIds);
}
constructor(g: GroupEntry) {
this.externalId = g.externalId;
this.displayName = g.displayName;
this.userIds = Array.from(g.userMemberExternalIds);
}
}

View File

@@ -1,22 +1,25 @@
import { GroupResponse } from './groupResponse';
import { UserResponse } from './userResponse';
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
import { SimResult } from '../simResult';
import { SimResult } from "../simResult";
import { BaseResponse } from 'jslib-node/cli/models/response/baseResponse';
import { GroupResponse } from "./groupResponse";
import { UserResponse } from "./userResponse";
export class TestResponse implements BaseResponse {
object: string;
groups: GroupResponse[] = [];
enabledUsers: UserResponse[] = [];
disabledUsers: UserResponse[] = [];
deletedUsers: UserResponse[] = [];
object: string;
groups: GroupResponse[] = [];
enabledUsers: UserResponse[] = [];
disabledUsers: UserResponse[] = [];
deletedUsers: UserResponse[] = [];
constructor(result: SimResult) {
this.object = 'test';
this.groups = result.groups != null ? result.groups.map(g => new GroupResponse(g)) : [];
this.enabledUsers = result.enabledUsers != null ? result.enabledUsers.map(u => new UserResponse(u)) : [];
this.disabledUsers = result.disabledUsers != null ? result.disabledUsers.map(u => new UserResponse(u)) : [];
this.deletedUsers = result.deletedUsers != null ? result.deletedUsers.map(u => new UserResponse(u)) : [];
}
constructor(result: SimResult) {
this.object = "test";
this.groups = result.groups != null ? result.groups.map((g) => new GroupResponse(g)) : [];
this.enabledUsers =
result.enabledUsers != null ? result.enabledUsers.map((u) => new UserResponse(u)) : [];
this.disabledUsers =
result.disabledUsers != null ? result.disabledUsers.map((u) => new UserResponse(u)) : [];
this.deletedUsers =
result.deletedUsers != null ? result.deletedUsers.map((u) => new UserResponse(u)) : [];
}
}

View File

@@ -1,11 +1,11 @@
import { UserEntry } from '../userEntry';
import { UserEntry } from "../userEntry";
export class UserResponse {
externalId: string;
displayName: string;
externalId: string;
displayName: string;
constructor(u: UserEntry) {
this.externalId = u.externalId;
this.displayName = u.displayName;
}
constructor(u: UserEntry) {
this.externalId = u.externalId;
this.displayName = u.displayName;
}
}

View File

@@ -1,10 +1,10 @@
import { GroupEntry } from './groupEntry';
import { UserEntry } from './userEntry';
import { GroupEntry } from "./groupEntry";
import { UserEntry } from "./userEntry";
export class SimResult {
groups: GroupEntry[] = [];
users: UserEntry[] = [];
enabledUsers: UserEntry[] = [];
disabledUsers: UserEntry[] = [];
deletedUsers: UserEntry[] = [];
groups: GroupEntry[] = [];
users: UserEntry[] = [];
enabledUsers: UserEntry[] = [];
disabledUsers: UserEntry[] = [];
deletedUsers: UserEntry[] = [];
}

View File

@@ -1,23 +1,23 @@
export class SyncConfiguration {
users = false;
groups = false;
interval = 5;
userFilter: string;
groupFilter: string;
removeDisabled = false;
overwriteExisting = false;
largeImport = false;
// Ldap properties
groupObjectClass: string;
userObjectClass: string;
groupPath: string;
userPath: string;
groupNameAttribute: string;
userEmailAttribute: string;
memberAttribute: string;
useEmailPrefixSuffix = false;
emailPrefixAttribute: string;
emailSuffix: string;
creationDateAttribute: string;
revisionDateAttribute: string;
users = false;
groups = false;
interval = 5;
userFilter: string;
groupFilter: string;
removeDisabled = false;
overwriteExisting = false;
largeImport = false;
// Ldap properties
groupObjectClass: string;
userObjectClass: string;
groupPath: string;
userPath: string;
groupNameAttribute: string;
userEmailAttribute: string;
memberAttribute: string;
useEmailPrefixSuffix = false;
emailPrefixAttribute: string;
emailSuffix: string;
creationDateAttribute: string;
revisionDateAttribute: string;
}

View File

@@ -1,15 +1,15 @@
import { Entry } from './entry';
import { Entry } from "./entry";
export class UserEntry extends Entry {
email: string;
disabled = false;
deleted = false;
email: string;
disabled = false;
deleted = false;
get displayName(): string {
if (this.email == null) {
return this.referenceId;
}
return this.email;
get displayName(): string {
if (this.email == null) {
return this.referenceId;
}
return this.email;
}
}

1784
src/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
{
"name": "bitwarden-directory-connector",
"name": "@bitwarden/directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2.9.6",
"version": "2.10.0",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
@@ -13,9 +13,10 @@
},
"dependencies": {
"browser-hrtime": "^1.1.8",
"electron-log": "4.3.5",
"electron-store": "8.0.0",
"electron-updater": "4.3.9",
"keytar": "7.6.0"
"electron-log": "4.4.1",
"electron-store": "8.0.1",
"electron-updater": "4.6.1",
"keytar": "7.7.0",
"rxjs": "^7.4.0"
}
}

View File

@@ -1,301 +1,319 @@
import * as chalk from 'chalk';
import * as program from 'commander';
import * as path from 'path';
import * as path from "path";
import { Main } from './bwdc';
import * as chalk from "chalk";
import * as program from "commander";
import { ClearCacheCommand } from './commands/clearCache.command';
import { ConfigCommand } from './commands/config.command';
import { LastSyncCommand } from './commands/lastSync.command';
import { SyncCommand } from './commands/sync.command';
import { TestCommand } from './commands/test.command';
import { Utils } from "jslib-common/misc/utils";
import { BaseProgram } from "jslib-node/cli/baseProgram";
import { LoginCommand } from "jslib-node/cli/commands/login.command";
import { LogoutCommand } from "jslib-node/cli/commands/logout.command";
import { UpdateCommand } from "jslib-node/cli/commands/update.command";
import { Response } from "jslib-node/cli/models/response";
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
import { LoginCommand } from 'jslib-node/cli/commands/login.command';
import { LogoutCommand } from 'jslib-node/cli/commands/logout.command';
import { UpdateCommand } from 'jslib-node/cli/commands/update.command';
import { Main } from "./bwdc";
import { ClearCacheCommand } from "./commands/clearCache.command";
import { ConfigCommand } from "./commands/config.command";
import { LastSyncCommand } from "./commands/lastSync.command";
import { SyncCommand } from "./commands/sync.command";
import { TestCommand } from "./commands/test.command";
import { BaseProgram } from 'jslib-node/cli/baseProgram';
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { Response } from 'jslib-node/cli/models/response';
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse';
import { Utils } from 'jslib-common/misc/utils';
const writeLn = (s: string, finalLine: boolean = false, error: boolean = false) => {
const stream = error ? process.stderr : process.stdout;
if (finalLine && process.platform === 'win32') {
stream.write(s);
} else {
stream.write(s + '\n');
}
const writeLn = (s: string, finalLine = false, error = false) => {
const stream = error ? process.stderr : process.stdout;
if (finalLine && process.platform === "win32") {
stream.write(s);
} else {
stream.write(s + "\n");
}
};
export class Program extends BaseProgram {
private apiKeyService: ApiKeyService;
constructor(private main: Main) {
super(main.stateService, writeLn);
}
constructor(private main: Main) {
super(main.userService, writeLn);
this.apiKeyService = main.apiKeyService;
}
async run() {
program
.option("--pretty", "Format output. JSON is tabbed with two spaces.")
.option("--raw", "Return raw output instead of a descriptive message.")
.option("--response", "Return a JSON formatted version of response output.")
.option("--cleanexit", "Exit with a success exit code (0) unless an error is thrown.")
.option("--quiet", "Don't return anything to stdout.")
.option("--nointeraction", "Do not prompt for interactive user input.")
.version(await this.main.platformUtilsService.getApplicationVersion(), "-v, --version");
async run() {
program
.option('--pretty', 'Format output. JSON is tabbed with two spaces.')
.option('--raw', 'Return raw output instead of a descriptive message.')
.option('--response', 'Return a JSON formatted version of response output.')
.option('--cleanexit', 'Exit with a success exit code (0) unless an error is thrown.')
.option('--quiet', 'Don\'t return anything to stdout.')
.option('--nointeraction', 'Do not prompt for interactive user input.')
.version(await this.main.platformUtilsService.getApplicationVersion(), '-v, --version');
program.on("option:pretty", () => {
process.env.BW_PRETTY = "true";
});
program.on('option:pretty', () => {
process.env.BW_PRETTY = 'true';
});
program.on("option:raw", () => {
process.env.BW_RAW = "true";
});
program.on('option:raw', () => {
process.env.BW_RAW = 'true';
});
program.on("option:quiet", () => {
process.env.BW_QUIET = "true";
});
program.on('option:quiet', () => {
process.env.BW_QUIET = 'true';
});
program.on("option:response", () => {
process.env.BW_RESPONSE = "true";
});
program.on('option:response', () => {
process.env.BW_RESPONSE = 'true';
});
program.on("option:cleanexit", () => {
process.env.BW_CLEANEXIT = "true";
});
program.on('option:cleanexit', () => {
process.env.BW_CLEANEXIT = 'true';
});
program.on("option:nointeraction", () => {
process.env.BW_NOINTERACTION = "true";
});
program.on('option:nointeraction', () => {
process.env.BW_NOINTERACTION = 'true';
});
program.on("command:*", () => {
writeLn(chalk.redBright("Invalid command: " + program.args.join(" ")), false, true);
writeLn("See --help for a list of available commands.", true, true);
process.exitCode = 1;
});
program.on('command:*', () => {
writeLn(chalk.redBright('Invalid command: ' + program.args.join(' ')), false, true);
writeLn('See --help for a list of available commands.', true, true);
process.exitCode = 1;
});
program.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bwdc login");
writeLn(" bwdc test");
writeLn(" bwdc sync");
writeLn(" bwdc last-sync");
writeLn(" bwdc config server https://bw.company.com");
writeLn(" bwdc update");
writeLn("", true);
});
program.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc login');
writeLn(' bwdc test');
writeLn(' bwdc sync');
writeLn(' bwdc last-sync');
writeLn(' bwdc config server https://bw.company.com');
writeLn(' bwdc update');
writeLn('', true);
});
program
.command("login [clientId] [clientSecret]")
.description("Log into an organization account.", {
clientId: "Client_id part of your organization's API key",
clientSecret: "Client_secret part of your organization's API key",
})
.action(async (clientId: string, clientSecret: string, options: program.OptionValues) => {
await this.exitIfAuthed();
const command = new LoginCommand(
this.main.authService,
this.main.apiService,
this.main.i18nService,
this.main.environmentService,
this.main.passwordGenerationService,
this.main.cryptoFunctionService,
this.main.platformUtilsService,
this.main.stateService,
this.main.cryptoService,
this.main.policyService,
this.main.twoFactorService,
"connector"
);
program
.command('login [clientId] [clientSecret]')
.description('Log into an organization account.', {
clientId: 'Client_id part of your organization\'s API key',
clientSecret: 'Client_secret part of your organization\'s API key',
})
.action(async (clientId: string, clientSecret: string, options: program.OptionValues) => {
await this.exitIfAuthed();
const command = new LoginCommand(this.main.authService, this.main.apiService, this.main.i18nService,
this.main.environmentService, this.main.passwordGenerationService, this.main.cryptoFunctionService,
this.main.platformUtilsService, 'connector');
if (!Utils.isNullOrWhitespace(clientId)) {
process.env.BW_CLIENTID = clientId;
}
if (!Utils.isNullOrWhitespace(clientSecret)) {
process.env.BW_CLIENTSECRET = clientSecret;
}
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
const response = await command.run(null, null, options);
this.processResponse(response);
});
program
.command('logout')
.description('Log out of the current user account.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc logout');
writeLn('', true);
})
.action(async () => {
await this.exitIfNotAuthed();
const command = new LogoutCommand(this.main.authService, this.main.i18nService,
async () => await this.main.logout());
const response = await command.run();
this.processResponse(response);
});
program
.command('test')
.description('Test a simulated sync.')
.option('-l, --last', 'Since the last successful sync.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc test');
writeLn(' bwdc test --last');
writeLn('', true);
})
.action(async (options: program.OptionValues) => {
await this.exitIfNotAuthed();
const command = new TestCommand(this.main.syncService, this.main.i18nService);
const response = await command.run(options);
this.processResponse(response);
});
program
.command('sync')
.description('Sync the directory.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc sync');
writeLn('', true);
})
.action(async () => {
await this.exitIfNotAuthed();
const command = new SyncCommand(this.main.syncService, this.main.i18nService);
const response = await command.run();
this.processResponse(response);
});
program
.command('last-sync <object>')
.description('Get the last successful sync date.')
.on('--help', () => {
writeLn('\n Notes:');
writeLn('');
writeLn(' Returns empty response if no sync has been performed for the given object.');
writeLn('');
writeLn(' Examples:');
writeLn('');
writeLn(' bwdc last-sync groups');
writeLn(' bwdc last-sync users');
writeLn('', true);
})
.action(async (object: string) => {
await this.exitIfNotAuthed();
const command = new LastSyncCommand(this.main.configurationService);
const response = await command.run(object);
this.processResponse(response);
});
program
.command('config <setting> [value]')
.description('Configure settings.')
.option('--secretenv <variable-name>', 'Read secret from the named environment variable.')
.option('--secretfile <filename>', 'Read secret from first line of the named file.')
.on('--help', () => {
writeLn('\n Settings:');
writeLn('');
writeLn(' server - On-premise hosted installation URL.');
writeLn(' directory - The type of directory to use.');
writeLn(' ldap.password - The password for connection to this LDAP server.');
writeLn(' azure.key - The Azure AD secret key.');
writeLn(' gsuite.key - The G Suite private key.');
writeLn(' okta.token - The Okta token.');
writeLn(' onelogin.secret - The OneLogin client secret.');
writeLn('');
writeLn(' Examples:');
writeLn('');
writeLn(' bwdc config server https://bw.company.com');
writeLn(' bwdc config server bitwarden.com');
writeLn(' bwdc config directory 1');
writeLn(' bwdc config ldap.password <password>');
writeLn(' bwdc config ldap.password --secretenv LDAP_PWD');
writeLn(' bwdc config azure.key <key>');
writeLn(' bwdc config gsuite.key <key>');
writeLn(' bwdc config okta.token <token>');
writeLn(' bwdc config onelogin.secret <secret>');
writeLn('', true);
})
.action(async (setting: string, value: string, options: program.OptionValues) => {
const command = new ConfigCommand(this.main.environmentService, this.main.i18nService,
this.main.configurationService);
const response = await command.run(setting, value, options);
this.processResponse(response);
});
program
.command('data-file')
.description('Path to data.json database file.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc data-file');
writeLn('', true);
})
.action(() => {
this.processResponse(
Response.success(new StringResponse(path.join(this.main.dataFilePath, 'data.json'))));
});
program
.command('clear-cache')
.description('Clear the sync cache.')
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bwdc clear-cache');
writeLn('', true);
})
.action(async (options: program.OptionValues) => {
const command = new ClearCacheCommand(this.main.configurationService, this.main.i18nService);
const response = await command.run(options);
this.processResponse(response);
});
program
.command('update')
.description('Check for updates.')
.on('--help', () => {
writeLn('\n Notes:');
writeLn('');
writeLn(' Returns the URL to download the newest version of this CLI tool.');
writeLn('');
writeLn(' Use the `--raw` option to return only the download URL for the update.');
writeLn('');
writeLn(' Examples:');
writeLn('');
writeLn(' bwdc update');
writeLn(' bwdc update --raw');
writeLn('', true);
})
.action(async () => {
const command = new UpdateCommand(this.main.platformUtilsService, this.main.i18nService,
'directory-connector', 'bwdc', false);
const response = await command.run();
this.processResponse(response);
});
program
.parse(process.argv);
if (process.argv.slice(2).length === 0) {
program.outputHelp();
if (!Utils.isNullOrWhitespace(clientId)) {
process.env.BW_CLIENTID = clientId;
}
}
async exitIfAuthed() {
const authed = await this.apiKeyService.isAuthenticated();
if (authed) {
const type = await this.apiKeyService.getEntityType();
const id = await this.apiKeyService.getEntityId();
this.processResponse(Response.error('You are already logged in as ' + type + '.' + id + '.'), true);
if (!Utils.isNullOrWhitespace(clientSecret)) {
process.env.BW_CLIENTSECRET = clientSecret;
}
}
async exitIfNotAuthed() {
const authed = await this.apiKeyService.isAuthenticated();
if (!authed) {
this.processResponse(Response.error('You are not logged in.'), true);
}
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
const response = await command.run(null, null, options);
this.processResponse(response);
});
program
.command("logout")
.description("Log out of the current user account.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bwdc logout");
writeLn("", true);
})
.action(async () => {
await this.exitIfNotAuthed();
const command = new LogoutCommand(
this.main.authService,
this.main.i18nService,
async () => await this.main.logout()
);
const response = await command.run();
this.processResponse(response);
});
program
.command("test")
.description("Test a simulated sync.")
.option("-l, --last", "Since the last successful sync.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bwdc test");
writeLn(" bwdc test --last");
writeLn("", true);
})
.action(async (options: program.OptionValues) => {
await this.exitIfNotAuthed();
const command = new TestCommand(this.main.syncService, this.main.i18nService);
const response = await command.run(options);
this.processResponse(response);
});
program
.command("sync")
.description("Sync the directory.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bwdc sync");
writeLn("", true);
})
.action(async () => {
await this.exitIfNotAuthed();
const command = new SyncCommand(this.main.syncService, this.main.i18nService);
const response = await command.run();
this.processResponse(response);
});
program
.command("last-sync <object>")
.description("Get the last successful sync date.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Returns empty response if no sync has been performed for the given object.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bwdc last-sync groups");
writeLn(" bwdc last-sync users");
writeLn("", true);
})
.action(async (object: string) => {
await this.exitIfNotAuthed();
const command = new LastSyncCommand(this.main.stateService);
const response = await command.run(object);
this.processResponse(response);
});
program
.command("config <setting> [value]")
.description("Configure settings.")
.option("--secretenv <variable-name>", "Read secret from the named environment variable.")
.option("--secretfile <filename>", "Read secret from first line of the named file.")
.on("--help", () => {
writeLn("\n Settings:");
writeLn("");
writeLn(" server - On-premise hosted installation URL.");
writeLn(" directory - The type of directory to use.");
writeLn(" ldap.password - The password for connection to this LDAP server.");
writeLn(" azure.key - The Azure AD secret key.");
writeLn(" gsuite.key - The G Suite private key.");
writeLn(" okta.token - The Okta token.");
writeLn(" onelogin.secret - The OneLogin client secret.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bwdc config server https://bw.company.com");
writeLn(" bwdc config server bitwarden.com");
writeLn(" bwdc config directory 1");
writeLn(" bwdc config ldap.password <password>");
writeLn(" bwdc config ldap.password --secretenv LDAP_PWD");
writeLn(" bwdc config azure.key <key>");
writeLn(" bwdc config gsuite.key <key>");
writeLn(" bwdc config okta.token <token>");
writeLn(" bwdc config onelogin.secret <secret>");
writeLn("", true);
})
.action(async (setting: string, value: string, options: program.OptionValues) => {
const command = new ConfigCommand(
this.main.environmentService,
this.main.i18nService,
this.main.stateService
);
const response = await command.run(setting, value, options);
this.processResponse(response);
});
program
.command("data-file")
.description("Path to data.json database file.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bwdc data-file");
writeLn("", true);
})
.action(() => {
this.processResponse(
Response.success(new StringResponse(path.join(this.main.dataFilePath, "data.json")))
);
});
program
.command("clear-cache")
.description("Clear the sync cache.")
.on("--help", () => {
writeLn("\n Examples:");
writeLn("");
writeLn(" bwdc clear-cache");
writeLn("", true);
})
.action(async (options: program.OptionValues) => {
const command = new ClearCacheCommand(this.main.i18nService, this.main.stateService);
const response = await command.run(options);
this.processResponse(response);
});
program
.command("update")
.description("Check for updates.")
.on("--help", () => {
writeLn("\n Notes:");
writeLn("");
writeLn(" Returns the URL to download the newest version of this CLI tool.");
writeLn("");
writeLn(" Use the `--raw` option to return only the download URL for the update.");
writeLn("");
writeLn(" Examples:");
writeLn("");
writeLn(" bwdc update");
writeLn(" bwdc update --raw");
writeLn("", true);
})
.action(async () => {
const command = new UpdateCommand(
this.main.platformUtilsService,
this.main.i18nService,
"directory-connector",
"bwdc",
false
);
const response = await command.run();
this.processResponse(response);
});
program.parse(process.argv);
if (process.argv.slice(2).length === 0) {
program.outputHelp();
}
}
async exitIfAuthed() {
const authed = await this.stateService.getIsAuthenticated();
if (authed) {
const type = await this.stateService.getEntityType();
const id = await this.stateService.getEntityId();
this.processResponse(
Response.error("You are already logged in as " + type + "." + id + "."),
true
);
}
}
async exitIfNotAuthed() {
const authed = await this.stateService.getIsAuthenticated();
if (!authed) {
this.processResponse(Response.error("You are not logged in."), true);
}
}
}

View File

@@ -1,5 +1,15 @@
$theme-colors: ( "primary": #175DDC, "primary-accent": #1252A3, "danger": #dd4b39, "success": #00a65a, "info": #555555, "warning": #bf7e16, "secondary": #ced4da, "secondary-alt": #1A3B66);
$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
$theme-colors: (
"primary": #175ddc,
"primary-accent": #1252a3,
"danger": #dd4b39,
"success": #00a65a,
"info": #555555,
"warning": #bf7e16,
"secondary": #ced4da,
"secondary-alt": #1a3b66,
);
$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
$h1-font-size: 2rem;
$h2-font-size: 1.3rem;
@@ -8,13 +18,13 @@ $h4-font-size: 1rem;
$h5-font-size: 1rem;
$h6-font-size: 1rem;
$primary: map_get($theme-colors, 'primary');
$primary-accent: map_get($theme-colors, 'primary-accent');
$success: map_get($theme-colors, 'success');
$info: map_get($theme-colors, 'info');
$warning: map_get($theme-colors, 'warning');
$danger: map_get($theme-colors, 'danger');
$secondary: map_get($theme-colors, 'secondary');
$secondary-alt: map_get($theme-colors, 'secondary-alt');
$primary: map_get($theme-colors, "primary");
$primary-accent: map_get($theme-colors, "primary-accent");
$success: map_get($theme-colors, "success");
$info: map_get($theme-colors, "info");
$warning: map_get($theme-colors, "warning");
$danger: map_get($theme-colors, "danger");
$secondary: map_get($theme-colors, "secondary");
$secondary-alt: map_get($theme-colors, "secondary-alt");
@import "~bootstrap/scss/bootstrap.scss";

View File

@@ -1,7 +1,7 @@
@import "~bootstrap/scss/_variables.scss";
html.os_windows {
body {
border-top: 1px solid $gray-400;
}
body {
border-top: 1px solid $gray-400;
}
}

View File

@@ -1,144 +1,143 @@
@import "~bootstrap/scss/_variables.scss";
body {
padding: 10px 0 20px 0;
padding: 10px 0 20px 0;
}
h1 {
border-bottom: 1px solid $border-color;
margin-bottom: 20px;
border-bottom: 1px solid $border-color;
margin-bottom: 20px;
small {
color: $text-muted;
font-size: $h1-font-size * .5;
}
small {
color: $text-muted;
font-size: $h1-font-size * 0.5;
}
}
h2 {
text-transform: uppercase;
font-weight: bold;
text-transform: uppercase;
font-weight: bold;
}
h3 {
text-transform: uppercase;
text-transform: uppercase;
}
h4 {
font-weight: bold;
font-weight: bold;
}
#duo-frame {
background: url('../images/loading.svg') 0 0 no-repeat;
height: 380px;
background: url("../images/loading.svg") 0 0 no-repeat;
height: 380px;
iframe {
width: 100%;
height: 100%;
border: none;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
}
app-root > #loading {
text-align: center;
margin-top: 20px;
color: $text-muted;
text-align: center;
margin-top: 20px;
color: $text-muted;
}
ul.testing-list {
ul {
padding-left: 18px;
}
ul {
padding-left: 18px;
}
li.deleted {
text-decoration: line-through;
}
li.deleted {
text-decoration: line-through;
}
}
.callout {
padding: 10px;
margin-bottom: 10px;
border: 1px solid #000000;
border-left-width: 5px;
border-radius: 3px;
border-color: #ddd;
background-color: white;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #000000;
border-left-width: 5px;
border-radius: 3px;
border-color: #ddd;
background-color: white;
.callout-heading {
margin-top: 0;
}
h3.callout-heading {
font-weight: bold;
text-transform: uppercase;
}
&.callout-primary {
border-left-color: $primary;
.callout-heading {
margin-top: 0;
color: $primary;
}
}
h3.callout-heading {
font-weight: bold;
text-transform: uppercase;
&.callout-info {
border-left-color: $info;
.callout-heading {
color: $info;
}
}
&.callout-primary {
border-left-color: $primary;
&.callout-danger {
border-left-color: $danger;
.callout-heading {
color: $primary;
}
.callout-heading {
color: $danger;
}
}
&.callout-info {
border-left-color: $info;
&.callout-success {
border-left-color: $success;
.callout-heading {
color: $info;
}
.callout-heading {
color: $success;
}
}
&.callout-danger {
border-left-color: $danger;
&.callout-warning {
border-left-color: $warning;
.callout-heading {
color: $danger;
}
.callout-heading {
color: $warning;
}
}
&.callout-success {
border-left-color: $success;
.callout-heading {
color: $success;
}
}
&.callout-warning {
border-left-color: $warning;
.callout-heading {
color: $warning;
}
}
ul {
padding-left: 40px;
margin: 0;
}
ul {
padding-left: 40px;
margin: 0;
}
}
.btn[class*="btn-outline-"] {
&:not(:hover) {
border-color: $secondary;
background-color: #fbfbfb;
}
&:not(:hover) {
border-color: $secondary;
background-color: #fbfbfb;
}
}
.btn-outline-secondary {
color: $text-muted;
color: $text-muted;
&:hover:not(:disabled) {
color: $body-color;
}
&:hover:not(:disabled) {
color: $body-color;
}
&:disabled {
opacity: 1;
}
&:disabled {
opacity: 1;
}
&:focus,
&.focus {
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), .5);
}
&:focus,
&.focus {
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), 0.5);
}
}

View File

@@ -1,130 +1,126 @@
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome.scss";
@import "~angular2-toaster/toaster";
@import "~ngx-toastr/toastr";
@import "~bootstrap/scss/_variables.scss";
#toast-container {
.toast-container {
.toast-close-button {
font-size: 18px;
margin-right: 4px;
}
.ngx-toastr {
align-items: center;
background-image: none !important;
border-radius: $border-radius;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
display: flex;
padding: 15px;
.toast-close-button {
right: -0.15em;
position: absolute;
right: 5px;
top: 0;
}
.toast {
opacity: 1 !important;
background-image: none !important;
border-radius: $border-radius;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
&:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
}
&:before {
font-family: FontAwesome;
font-size: 25px;
line-height: 20px;
float: left;
color: #ffffff;
padding-right: 10px;
margin: auto 0 auto -36px;
}
.toaster-icon {
display: none;
}
.toast-message {
p {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
&.toast-danger, &.toast-error {
background-image: none !important;
background-color: $danger;
&:before {
content: "\f0e7";
margin-left: -30px;
}
}
&.toast-warning {
background-image: none !important;
background-color: $warning;
&:before {
content: "\f071";
}
}
&.toast-info {
background-image: none !important;
background-color: $info;
&:before {
content: "\f05a";
}
}
&.toast-success {
background-image: none !important;
background-color: $success;
&:before {
content: "\f00C";
}
}
&:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
}
.icon i::before {
float: left;
font-style: normal;
font-family: $icomoon-font-family;
font-size: 25px;
line-height: 20px;
padding-right: 15px;
}
.toast-message {
p {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
&.toast-danger,
&.toast-error {
background-color: $danger;
.icon i::before {
content: map_get($icons, "error");
}
}
&.toast-warning {
background-color: $warning;
.icon i::before {
content: map_get($icons, "exclamation-triangle");
}
}
&.toast-info {
background-color: $info;
.icon i:before {
content: map_get($icons, "info-circle");
}
}
&.toast-success {
background-color: $success;
.icon i:before {
content: map_get($icons, "check");
}
}
}
}
@keyframes modalshow {
0% {
opacity: 0;
transform: translate(0, -25%);
}
0% {
opacity: 0;
transform: translate(0, -25%);
}
100% {
opacity: 1;
transform: translate(0, 0);
}
100% {
opacity: 1;
transform: translate(0, 0);
}
}
@keyframes backdropshow {
0% {
opacity: 0;
}
0% {
opacity: 0;
}
100% {
opacity: $modal-backdrop-opacity;
}
100% {
opacity: $modal-backdrop-opacity;
}
}
.modal {
display: block !important;
opacity: 1 !important;
display: block !important;
opacity: 1 !important;
}
.modal-dialog {
.modal.fade & {
transform: initial !important;
animation: modalshow 0.3s ease-in;
}
.modal.show & {
transform: initial !important;
}
transform: translate(0, 0);
.modal.fade & {
transform: initial !important;
animation: modalshow 0.3s ease-in;
}
.modal.show & {
transform: initial !important;
}
transform: translate(0, 0);
}
.modal-backdrop {
&.fade {
animation: backdropshow 0.1s ease-in;
}
opacity: $modal-backdrop-opacity !important;
&.fade {
animation: backdropshow 0.1s ease-in;
}
opacity: $modal-backdrop-opacity !important;
}

View File

@@ -1,4 +1,5 @@
@import "../../jslib/angular/src/scss/webfonts.css";
@import "../../jslib/angular/src/scss/bwicons/styles/style.scss";
@import "bootstrap.scss";
@import "pages.scss";
@import "misc.scss";

View File

@@ -1,31 +1,37 @@
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { AuthService } from "jslib-common/abstractions/auth.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { ApiLogInCredentials } from "jslib-common/models/domain/logInCredentials";
import { ApiService as ApiServiceBase } from "jslib-common/services/api.service";
import { ApiService as ApiServiceBase } from 'jslib-common/services/api.service';
import { StateService } from "../abstractions/state.service";
export async function refreshToken(apiKeyService: ApiKeyService, authService: AuthService) {
try {
const clientId = await apiKeyService.getClientId();
const clientSecret = await apiKeyService.getClientSecret();
if (clientId != null && clientSecret != null) {
await authService.logInApiKey(clientId, clientSecret);
}
} catch (e) {
return Promise.reject(e);
export async function refreshToken(stateService: StateService, authService: AuthService) {
try {
const clientId = await stateService.getApiKeyClientId();
const clientSecret = await stateService.getApiKeyClientSecret();
if (clientId != null && clientSecret != null) {
await authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
}
} catch (e) {
return Promise.reject(e);
}
}
export class ApiService extends ApiServiceBase {
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
private refreshTokenCallback: () => Promise<void>, logoutCallback: (expired: boolean) => Promise<void>,
customUserAgent: string = null) {
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent);
}
constructor(
tokenService: TokenService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
private refreshTokenCallback: () => Promise<void>,
logoutCallback: (expired: boolean) => Promise<void>,
customUserAgent: string = null
) {
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent);
}
doRefreshToken(): Promise<void> {
return this.refreshTokenCallback();
}
doRefreshToken(): Promise<void> {
return this.refreshTokenCallback();
}
}

View File

@@ -1,61 +1,65 @@
import { ApiService } from 'jslib-common/abstractions/api.service';
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { AppIdService } from 'jslib-common/abstractions/appId.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { AppIdService } from "jslib-common/abstractions/appId.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
import { AuthResult } from "jslib-common/models/domain/authResult";
import { ApiLogInCredentials } from "jslib-common/models/domain/logInCredentials";
import { AuthService as AuthServiceBase } from "jslib-common/services/auth.service";
import { AuthService as AuthServiceBase } from 'jslib-common/services/auth.service';
import { AuthResult } from 'jslib-common/models/domain/authResult';
import { DeviceRequest } from 'jslib-common/models/request/deviceRequest';
import { TokenRequest } from 'jslib-common/models/request/tokenRequest';
import { IdentityTokenResponse } from 'jslib-common/models/response/identityTokenResponse';
import { StateService } from "../abstractions/state.service";
import { OrganizationLogInStrategy } from "../misc/logInStrategies/organizationLogIn.strategy";
export class AuthService extends AuthServiceBase {
constructor(
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
keyConnectorService: KeyConnectorService,
environmentService: EnvironmentService,
stateService: StateService,
twoFactorService: TwoFactorService,
i18nService: I18nService
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
keyConnectorService,
environmentService,
stateService,
twoFactorService,
i18nService
);
}
constructor(cryptoService: CryptoService, apiService: ApiService, userService: UserService,
tokenService: TokenService, appIdService: AppIdService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
vaultTimeoutService: VaultTimeoutService, logService: LogService, private apiKeyService: ApiKeyService,
setCryptoKeys = true) {
super(cryptoService, apiService, userService, tokenService, appIdService, i18nService, platformUtilsService,
messagingService, vaultTimeoutService, logService, setCryptoKeys);
}
async logIn(credentials: ApiLogInCredentials): Promise<AuthResult> {
const strategy = new OrganizationLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService
);
async logInApiKey(clientId: string, clientSecret: string): Promise<AuthResult> {
this.selectedTwoFactorProviderType = null;
if (clientId.startsWith('organization')) {
return await this.organizationLogInHelper(clientId, clientSecret);
}
return await super.logInApiKey(clientId, clientSecret);
}
async logOut(callback: Function) {
this.apiKeyService.clear();
super.logOut(callback);
}
private async organizationLogInHelper(clientId: string, clientSecret: string) {
const appId = await this.appIdService.getAppId();
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
const request = new TokenRequest(null, null, [clientId, clientSecret], null,
null, false, null, deviceRequest);
const response = await this.apiService.postIdentityToken(request);
const result = new AuthResult();
result.twoFactor = !(response as any).accessToken;
const tokenResponse = response as IdentityTokenResponse;
result.resetMasterPassword = tokenResponse.resetMasterPassword;
await this.tokenService.setToken(tokenResponse.accessToken);
await this.apiKeyService.setInformation(clientId, clientSecret);
return result;
}
return strategy.logIn(credentials);
}
}

View File

@@ -1,464 +1,531 @@
import * as graph from '@microsoft/microsoft-graph-client';
import * as graphType from '@microsoft/microsoft-graph-types';
import * as https from 'https';
import * as querystring from 'querystring';
import * as https from "https";
import * as querystring from "querystring";
import { DirectoryType } from '../enums/directoryType';
import * as graph from "@microsoft/microsoft-graph-client";
import * as graphType from "@microsoft/microsoft-graph-types";
import { AzureConfiguration } from '../models/azureConfiguration';
import { GroupEntry } from '../models/groupEntry';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { BaseDirectoryService } from './baseDirectory.service';
import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { AzureConfiguration } from "../models/azureConfiguration";
import { GroupEntry } from "../models/groupEntry";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service";
const NextLink = '@odata.nextLink';
const DeltaLink = '@odata.deltaLink';
const ObjectType = '@odata.type';
const UserSelectParams = '?$select=id,mail,userPrincipalName,displayName,accountEnabled';
const AzurePublicIdentityAuhtority = "login.microsoftonline.com";
const AzureGovermentIdentityAuhtority = "login.microsoftonline.us";
const NextLink = "@odata.nextLink";
const DeltaLink = "@odata.deltaLink";
const ObjectType = "@odata.type";
const UserSelectParams = "?$select=id,mail,userPrincipalName,displayName,accountEnabled";
enum UserSetType {
IncludeUser,
ExcludeUser,
IncludeGroup,
ExcludeGroup,
IncludeUser,
ExcludeUser,
IncludeGroup,
ExcludeGroup,
}
export class AzureDirectoryService extends BaseDirectoryService implements IDirectoryService {
private client: graph.Client;
private dirConfig: AzureConfiguration;
private syncConfig: SyncConfiguration;
private accessToken: string;
private accessTokenExpiration: Date;
private client: graph.Client;
private dirConfig: AzureConfiguration;
private syncConfig: SyncConfiguration;
private accessToken: string;
private accessTokenExpiration: Date;
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) {
super();
this.init();
constructor(
private logService: LogService,
private i18nService: I18nService,
private stateService: StateService
) {
super();
this.init();
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.stateService.getDirectoryType();
if (type !== DirectoryType.AzureActiveDirectory) {
return;
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.AzureActiveDirectory) {
return;
}
this.dirConfig = await this.configurationService.getDirectory<AzureConfiguration>(
DirectoryType.AzureActiveDirectory);
if (this.dirConfig == null) {
return;
}
this.syncConfig = await this.configurationService.getSync();
if (this.syncConfig == null) {
return;
}
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getCurrentUsers();
const deletedUsers = await this.getDeletedUsers(force, !test);
users = users.concat(deletedUsers);
}
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = await this.createAadCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
this.dirConfig = await this.stateService.getDirectory<AzureConfiguration>(
DirectoryType.AzureActiveDirectory
);
if (this.dirConfig == null) {
return;
}
private async getCurrentUsers(): Promise<UserEntry[]> {
const entryIds = new Set<string>();
const entries: UserEntry[] = [];
const userReq = this.client.api('/users' + UserSelectParams);
let res = await userReq.get();
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
while (true) {
const users: graphType.User[] = res.value;
if (users != null) {
for (const user of users) {
if (user.id == null || entryIds.has(user.id)) {
continue;
}
const entry = this.buildUser(user);
if (await this.filterOutUserResult(setFilter, entry, true)) {
continue;
}
if (!entry.disabled && !entry.deleted &&
(entry.email == null || entry.email.indexOf('#') > -1)) {
continue;
}
entries.push(entry);
entryIds.add(user.id);
}
}
if (res[NextLink] == null) {
break;
} else {
const nextReq = this.client.api(res[NextLink]);
res = await nextReq.get();
}
}
return entries;
this.syncConfig = await this.stateService.getSync();
if (this.syncConfig == null) {
return;
}
private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
const entryIds = new Set<string>();
const entries: UserEntry[] = [];
let res: any = null;
const token = await this.configurationService.getUserDeltaToken();
if (!force && token != null) {
try {
const deltaReq = this.client.api(token);
res = await deltaReq.get();
} catch {
res = null;
}
}
if (res == null) {
const userReq = this.client.api('/users/delta' + UserSelectParams);
res = await userReq.get();
}
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
while (true) {
const users: graphType.User[] = res.value;
if (users != null) {
for (const user of users) {
if (user.id == null || entryIds.has(user.id)) {
continue;
}
const entry = this.buildUser(user);
if (!entry.deleted) {
continue;
}
if (await this.filterOutUserResult(setFilter, entry, false)) {
continue;
}
entries.push(entry);
entryIds.add(user.id);
}
}
if (res[NextLink] == null) {
if (res[DeltaLink] != null && saveDelta) {
await this.configurationService.saveUserDeltaToken(res[DeltaLink]);
}
break;
} else {
const nextReq = this.client.api(res[NextLink]);
res = await nextReq.get();
}
}
return entries;
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getCurrentUsers();
const deletedUsers = await this.getDeletedUsers(force, !test);
users = users.concat(deletedUsers);
}
private async createAadCustomSet(filter: string): Promise<[boolean, Set<string>]> {
if (filter == null || filter === '') {
return null;
}
const mainParts = filter.split('|');
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') {
return null;
}
const parts = mainParts[0].split(':');
if (parts.length !== 2) {
return null;
}
const keyword = parts[0].trim().toLowerCase();
let exclude = true;
if (keyword === 'include') {
exclude = false;
} else if (keyword === 'exclude') {
exclude = true;
} else if (keyword === 'excludeadministrativeunit') {
exclude = true;
} else if (keyword === 'includeadministrativeunit') {
exclude = false;
} else {
return null;
}
const set = new Set<string>();
const pieces = parts[1].split(',');
if (keyword === 'excludeadministrativeunit' || keyword === 'includeadministrativeunit') {
for (const p of pieces) {
const auMembers = await this.client
.api(`https://graph.microsoft.com/v1.0/directory/administrativeUnits/${p}/members`).get();
for (const auMember of auMembers.value) {
if (auMember['@odata.type'] === '#microsoft.graph.group') {
set.add(auMember.displayName.toLowerCase());
}
}
}
} else {
for (const p of pieces) {
set.add(p.trim().toLowerCase());
}
}
return [exclude, set];
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = await this.createAadCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
private createCustomUserSet(filter: string): [UserSetType, Set<string>] {
if (filter == null || filter === '') {
return null;
}
return [groups, users];
}
const mainParts = filter.split('|');
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') {
return null;
}
private async getCurrentUsers(): Promise<UserEntry[]> {
let entries: UserEntry[] = [];
let users: graphType.User[];
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
const userIdsToExclude = new Set<string>();
const parts = mainParts[0].split(':');
if (parts.length !== 2) {
return null;
}
// Only get users for the groups provided in includeGroup filter
if (setFilter != null && setFilter[0] === UserSetType.IncludeGroup) {
users = await this.getUsersByGroups(setFilter);
// Get the users in the excludedGroups and filter them out from all users
} else if (setFilter != null && setFilter[0] === UserSetType.ExcludeGroup) {
(await this.getUsersByGroups(setFilter)).forEach((user: graphType.User) =>
userIdsToExclude.add(user.id)
);
const userReq = this.client.api("/users" + UserSelectParams);
users = await this.getUsersByResource(userReq);
} else {
const userReq = this.client.api("/users" + UserSelectParams);
users = await this.getUsersByResource(userReq);
}
if (users != null) {
entries = await this.buildUserEntries(users, userIdsToExclude, setFilter);
}
return entries;
}
const keyword = parts[0].trim().toLowerCase();
let userSetType = UserSetType.IncludeUser;
if (keyword === 'include') {
userSetType = UserSetType.IncludeUser;
} else if (keyword === 'exclude') {
userSetType = UserSetType.ExcludeUser;
} else if (keyword === 'includegroup') {
userSetType = UserSetType.IncludeGroup;
} else if (keyword === 'excludegroup') {
userSetType = UserSetType.ExcludeGroup;
} else {
return null;
}
private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
const entryIds = new Set<string>();
const entries: UserEntry[] = [];
const set = new Set<string>();
const pieces = parts[1].split(',');
for (const p of pieces) {
set.add(p.trim().toLowerCase());
}
return [userSetType, set];
let res: any = null;
const token = await this.stateService.getUserDelta();
if (!force && token != null) {
try {
const deltaReq = this.client.api(token);
res = await deltaReq.get();
} catch {
res = null;
}
}
private async filterOutUserResult(setFilter: [UserSetType, Set<string>], user: UserEntry,
checkGroupsFilter: boolean): Promise<boolean> {
if (setFilter == null) {
return false;
if (res == null) {
const userReq = this.client.api("/users/delta" + UserSelectParams);
res = await userReq.get();
}
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
// eslint-disable-next-line
while (true) {
const users: graphType.User[] = res.value;
if (users != null) {
for (const user of users) {
if (user.id == null || entryIds.has(user.id)) {
continue;
}
const entry = this.buildUser(user);
if (!entry.deleted) {
continue;
}
if (
setFilter != null &&
(setFilter[0] === UserSetType.IncludeUser ||
setFilter[0] === UserSetType.ExcludeUser) &&
(await this.filterOutUserResult(setFilter, entry))
) {
continue;
}
entries.push(entry);
entryIds.add(user.id);
}
}
if (res[NextLink] == null) {
if (res[DeltaLink] != null && saveDelta) {
await this.stateService.setUserDelta(res[DeltaLink]);
}
break;
} else {
const nextReq = this.client.api(res[NextLink]);
res = await nextReq.get();
}
}
return entries;
}
private async createAadCustomSet(filter: string): Promise<[boolean, Set<string>]> {
if (filter == null || filter === "") {
return null;
}
const mainParts = filter.split("|");
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
return null;
}
const parts = mainParts[0].split(":");
if (parts.length !== 2) {
return null;
}
const keyword = parts[0].trim().toLowerCase();
let exclude = true;
if (keyword === "include") {
exclude = false;
} else if (keyword === "exclude") {
exclude = true;
} else if (keyword === "excludeadministrativeunit") {
exclude = true;
} else if (keyword === "includeadministrativeunit") {
exclude = false;
} else {
return null;
}
const set = new Set<string>();
const pieces = parts[1].split(",");
if (keyword === "excludeadministrativeunit" || keyword === "includeadministrativeunit") {
for (const p of pieces) {
const auMembers = await this.client
.api(`https://graph.microsoft.com/v1.0/directory/administrativeUnits/${p}/members`)
.get();
for (const auMember of auMembers.value) {
if (auMember["@odata.type"] === "#microsoft.graph.group") {
set.add(auMember.displayName.toLowerCase());
}
}
}
} else {
for (const p of pieces) {
set.add(p.trim().toLowerCase());
}
}
return [exclude, set];
}
private createCustomUserSet(filter: string): [UserSetType, Set<string>] {
if (filter == null || filter === "") {
return null;
}
const mainParts = filter.split("|");
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
return null;
}
const parts = mainParts[0].split(":");
if (parts.length !== 2) {
return null;
}
const keyword = parts[0].trim().toLowerCase();
let userSetType = UserSetType.IncludeUser;
if (keyword === "include") {
userSetType = UserSetType.IncludeUser;
} else if (keyword === "exclude") {
userSetType = UserSetType.ExcludeUser;
} else if (keyword === "includegroup") {
userSetType = UserSetType.IncludeGroup;
} else if (keyword === "excludegroup") {
userSetType = UserSetType.ExcludeGroup;
} else {
return null;
}
const set = new Set<string>();
const pieces = parts[1].split(",");
for (const p of pieces) {
set.add(p.trim().toLowerCase());
}
return [userSetType, set];
}
private async filterOutUserResult(
setFilter: [UserSetType, Set<string>],
user: UserEntry
): Promise<boolean> {
if (setFilter == null) {
return false;
}
let userSetTypeExclude = null;
if (setFilter[0] === UserSetType.IncludeUser) {
userSetTypeExclude = false;
} else if (setFilter[0] === UserSetType.ExcludeUser) {
userSetTypeExclude = true;
}
if (userSetTypeExclude != null) {
return this.filterOutResult([userSetTypeExclude, setFilter[1]], user.email);
}
return false;
}
private buildUser(user: graphType.User): UserEntry {
const entry = new UserEntry();
entry.referenceId = user.id;
entry.externalId = user.id;
entry.email = user.mail;
if (
user.userPrincipalName &&
(entry.email == null || entry.email === "" || entry.email.indexOf("onmicrosoft.com") > -1)
) {
entry.email = user.userPrincipalName;
}
if (entry.email != null) {
entry.email = entry.email.trim().toLowerCase();
}
entry.disabled = user.accountEnabled == null ? false : !user.accountEnabled;
if ((user as any)["@removed"] != null && (user as any)["@removed"].reason === "changed") {
entry.deleted = true;
}
return entry;
}
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
const entryIds = new Set<string>();
const entries: GroupEntry[] = [];
const groupsReq = this.client.api("/groups");
let res = await groupsReq.get();
// eslint-disable-next-line
while (true) {
const groups: graphType.Group[] = res.value;
if (groups != null) {
for (const group of groups) {
if (group.id == null || entryIds.has(group.id)) {
continue;
}
if (this.filterOutResult(setFilter, group.displayName)) {
continue;
}
const entry = await this.buildGroup(group);
entries.push(entry);
entryIds.add(group.id);
}
}
if (res[NextLink] == null) {
break;
} else {
const nextReq = this.client.api(res[NextLink]);
res = await nextReq.get();
}
}
return entries;
}
private async getUsersByResource(usersRequest: graph.GraphRequest) {
const users: graphType.User[] = [];
let res = await usersRequest.get();
res.value.forEach((user: graphType.User) => users.push(user));
while (res[NextLink] != null) {
const nextReq = this.client.api(res[NextLink]);
res = await nextReq.get();
res.value.forEach((user: graphType.User) => users.push(user));
}
return users;
}
private async getUsersByGroups(setFilter: [UserSetType, Set<string>]): Promise<graphType.User[]> {
const users: graphType.User[] = [];
for (const group of setFilter[1]) {
const groupUsersReq = this.client.api(
`/groups/${group}/transitiveMembers` + UserSelectParams
);
users.push(...(await this.getUsersByResource(groupUsersReq)));
}
return users;
}
private async buildUserEntries(
users: graphType.User[],
userIdsToExclude: Set<string>,
setFilter: [UserSetType, Set<string>]
) {
const entryIds = new Set<string>();
const entries: UserEntry[] = [];
for (const user of users) {
if (user.id == null || entryIds.has(user.id) || userIdsToExclude.has(user.id)) {
continue;
}
const entry = this.buildUser(user);
if (
setFilter != null &&
(setFilter[0] === UserSetType.IncludeUser || setFilter[0] === UserSetType.ExcludeUser) &&
(await this.filterOutUserResult(setFilter, entry))
) {
continue;
}
if (!this.isInvalidUser(entry)) {
entries.push(entry);
entryIds.add(user.id);
}
}
return entries;
}
private isInvalidUser(user: UserEntry): boolean {
return !user.disabled && !user.deleted && (user.email == null || user.email.indexOf("#") > -1);
}
private async buildGroup(group: graphType.Group): Promise<GroupEntry> {
const entry = new GroupEntry();
entry.referenceId = group.id;
entry.externalId = group.id;
entry.name = group.displayName;
const memReq = this.client.api("/groups/" + group.id + "/members");
let memRes = await memReq.get();
// eslint-disable-next-line
while (true) {
const members: any = memRes.value;
if (members != null) {
for (const member of members) {
if (member[ObjectType] === "#microsoft.graph.group") {
entry.groupMemberReferenceIds.add((member as graphType.Group).id);
} else if (member[ObjectType] === "#microsoft.graph.user") {
entry.userMemberExternalIds.add((member as graphType.User).id);
}
}
}
if (memRes[NextLink] == null) {
break;
} else {
const nextMemReq = this.client.api(memRes[NextLink]);
memRes = await nextMemReq.get();
}
}
return entry;
}
private init() {
this.client = graph.Client.init({
authProvider: (done) => {
if (
this.dirConfig.applicationId == null ||
this.dirConfig.key == null ||
this.dirConfig.tenant == null
) {
done(new Error(this.i18nService.t("dirConfigIncomplete")), null);
return;
}
let userSetTypeExclude = null;
if (setFilter[0] === UserSetType.IncludeUser) {
userSetTypeExclude = false;
} else if (setFilter[0] === UserSetType.ExcludeUser) {
userSetTypeExclude = true;
const identityAuthority =
this.dirConfig.identityAuthority != null
? this.dirConfig.identityAuthority
: AzurePublicIdentityAuhtority;
if (
identityAuthority !== AzurePublicIdentityAuhtority &&
identityAuthority !== AzureGovermentIdentityAuhtority
) {
done(new Error(this.i18nService.t("dirConfigIncomplete")), null);
return;
}
if (userSetTypeExclude != null) {
return this.filterOutResult([userSetTypeExclude, setFilter[1]], user.email);
if (!this.accessTokenIsExpired()) {
done(null, this.accessToken);
return;
}
// We need to *not* call the /checkMemberGroups method for deleted users, it will always fail
if (!checkGroupsFilter) {
return false;
}
const memberGroups = await this.client.api(`/users/${user.externalId}/checkMemberGroups`).post({
groupIds: Array.from(setFilter[1]),
this.accessToken = null;
this.accessTokenExpiration = null;
const data = querystring.stringify({
client_id: this.dirConfig.applicationId,
client_secret: this.dirConfig.key,
grant_type: "client_credentials",
scope: "https://graph.microsoft.com/.default",
});
if (memberGroups.value.length > 0 && setFilter[0] === UserSetType.IncludeGroup) {
return false;
} else if (memberGroups.value.length > 0 && setFilter[0] === UserSetType.ExcludeGroup) {
return true;
} else if (memberGroups.value.length === 0 && setFilter[0] === UserSetType.IncludeGroup) {
return true;
} else if (memberGroups.value.length === 0 && setFilter[0] === UserSetType.ExcludeGroup) {
return false;
}
return false;
}
private buildUser(user: graphType.User): UserEntry {
const entry = new UserEntry();
entry.referenceId = user.id;
entry.externalId = user.id;
entry.email = user.mail;
if (user.userPrincipalName && (entry.email == null || entry.email === '' ||
entry.email.indexOf('onmicrosoft.com') > -1)) {
entry.email = user.userPrincipalName;
}
if (entry.email != null) {
entry.email = entry.email.trim().toLowerCase();
}
entry.disabled = user.accountEnabled == null ? false : !user.accountEnabled;
if ((user as any)['@removed'] != null && (user as any)['@removed'].reason === 'changed') {
entry.deleted = true;
}
return entry;
}
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
const entryIds = new Set<string>();
const entries: GroupEntry[] = [];
const groupsReq = this.client.api('/groups');
let res = await groupsReq.get();
while (true) {
const groups: graphType.Group[] = res.value;
if (groups != null) {
for (const group of groups) {
if (group.id == null || entryIds.has(group.id)) {
continue;
}
if (this.filterOutResult(setFilter, group.displayName)) {
continue;
}
const entry = await this.buildGroup(group);
entries.push(entry);
entryIds.add(group.id);
}
}
if (res[NextLink] == null) {
break;
} else {
const nextReq = this.client.api(res[NextLink]);
res = await nextReq.get();
}
}
return entries;
}
private async buildGroup(group: graphType.Group): Promise<GroupEntry> {
const entry = new GroupEntry();
entry.referenceId = group.id;
entry.externalId = group.id;
entry.name = group.displayName;
const memReq = this.client.api('/groups/' + group.id + '/members');
let memRes = await memReq.get();
while (true) {
const members: any = memRes.value;
if (members != null) {
for (const member of members) {
if (member[ObjectType] === '#microsoft.graph.group') {
entry.groupMemberReferenceIds.add((member as graphType.Group).id);
} else if (member[ObjectType] === '#microsoft.graph.user') {
entry.userMemberExternalIds.add((member as graphType.User).id);
}
}
}
if (memRes[NextLink] == null) {
break;
} else {
const nextMemReq = this.client.api(memRes[NextLink]);
memRes = await nextMemReq.get();
}
}
return entry;
}
private init() {
this.client = graph.Client.init({
authProvider: done => {
if (this.dirConfig.applicationId == null || this.dirConfig.key == null ||
this.dirConfig.tenant == null) {
done(new Error(this.i18nService.t('dirConfigIncomplete')), null);
return;
}
if (!this.accessTokenIsExpired()) {
done(null, this.accessToken);
return;
}
this.accessToken = null;
this.accessTokenExpiration = null;
const data = querystring.stringify({
client_id: this.dirConfig.applicationId,
client_secret: this.dirConfig.key,
grant_type: 'client_credentials',
scope: 'https://graph.microsoft.com/.default',
});
const req = https.request({
host: 'login.microsoftonline.com',
path: '/' + this.dirConfig.tenant + '/oauth2/v2.0/token',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(data),
},
}, res => {
res.setEncoding('utf8');
res.on('data', (chunk: string) => {
const d = JSON.parse(chunk);
if (res.statusCode === 200 && d.access_token != null) {
this.setAccessTokenExpiration(d.access_token, d.expires_in);
done(null, d.access_token);
} else if (d.error != null && d.error_description != null) {
const shortError = d.error_description?.split('\n', 1)[0];
const err = new Error(d.error + ' (' + res.statusCode + '): ' + shortError);
// tslint: disable-next-line
console.error(d.error_description);
done(err, null);
} else {
const err = new Error('Unknown error (' + res.statusCode + ').');
done(err, null);
}
});
}).on('error', err => {
done(err, null);
});
req.write(data);
req.end();
const req = https
.request(
{
host: identityAuthority,
path: "/" + this.dirConfig.tenant + "/oauth2/v2.0/token",
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": Buffer.byteLength(data),
},
},
});
(res) => {
res.setEncoding("utf8");
res.on("data", (chunk: string) => {
const d = JSON.parse(chunk);
if (res.statusCode === 200 && d.access_token != null) {
this.setAccessTokenExpiration(d.access_token, d.expires_in);
done(null, d.access_token);
} else if (d.error != null && d.error_description != null) {
const shortError = d.error_description?.split("\n", 1)[0];
const err = new Error(d.error + " (" + res.statusCode + "): " + shortError);
// eslint-disable-next-line
console.error(d.error_description);
done(err, null);
} else {
const err = new Error("Unknown error (" + res.statusCode + ").");
done(err, null);
}
});
}
)
.on("error", (err) => {
done(err, null);
});
req.write(data);
req.end();
},
});
}
private accessTokenIsExpired() {
if (this.accessToken == null || this.accessTokenExpiration == null) {
return true;
}
private accessTokenIsExpired() {
if (this.accessToken == null || this.accessTokenExpiration == null) {
return true;
}
// expired if less than 2 minutes til expiration
const now = new Date();
return this.accessTokenExpiration.getTime() - now.getTime() < 120000;
}
// expired if less than 2 minutes til expiration
const now = new Date();
return this.accessTokenExpiration.getTime() - now.getTime() < 120000;
private setAccessTokenExpiration(accessToken: string, expSeconds: number) {
if (accessToken == null || expSeconds == null) {
return;
}
private setAccessTokenExpiration(accessToken: string, expSeconds: number) {
if (accessToken == null || expSeconds == null) {
return;
}
this.accessToken = accessToken;
const exp = new Date();
exp.setSeconds(exp.getSeconds() + expSeconds);
this.accessTokenExpiration = exp;
}
this.accessToken = accessToken;
const exp = new Date();
exp.setSeconds(exp.getSeconds() + expSeconds);
this.accessTokenExpiration = exp;
}
}

View File

@@ -1,91 +1,94 @@
import { SyncConfiguration } from '../models/syncConfiguration';
import { GroupEntry } from '../models/groupEntry';
import { UserEntry } from '../models/userEntry';
import { GroupEntry } from "../models/groupEntry";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
export abstract class BaseDirectoryService {
protected createDirectoryQuery(filter: string) {
if (filter == null || filter === '') {
return null;
}
const mainParts = filter.split('|');
if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === '') {
return null;
}
return mainParts[1].trim();
protected createDirectoryQuery(filter: string) {
if (filter == null || filter === "") {
return null;
}
protected createCustomSet(filter: string): [boolean, Set<string>] {
if (filter == null || filter === '') {
return null;
}
const mainParts = filter.split('|');
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') {
return null;
}
const parts = mainParts[0].split(':');
if (parts.length !== 2) {
return null;
}
const keyword = parts[0].trim().toLowerCase();
let exclude = true;
if (keyword === 'include') {
exclude = false;
} else if (keyword === 'exclude') {
exclude = true;
} else {
return null;
}
const set = new Set<string>();
const pieces = parts[1].split(',');
for (const p of pieces) {
set.add(p.trim().toLowerCase());
}
return [exclude, set];
const mainParts = filter.split("|");
if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === "") {
return null;
}
protected filterOutResult(setFilter: [boolean, Set<string>], result: string) {
if (setFilter != null) {
const cleanResult = result != null ? result.trim().toLowerCase() : '--';
const excluded = setFilter[0];
const set = setFilter[1];
return mainParts[1].trim();
}
if (excluded && set.has(cleanResult)) {
return true;
} else if (!excluded && !set.has(cleanResult)) {
return true;
}
}
return false;
protected createCustomSet(filter: string): [boolean, Set<string>] {
if (filter == null || filter === "") {
return null;
}
protected filterUsersFromGroupsSet(users: UserEntry[], groups: GroupEntry[],
setFilter: [boolean, Set<string>], syncConfig: SyncConfiguration): UserEntry[] {
if (setFilter == null || users == null) {
return users;
}
return users.filter(u => {
if (u.deleted) {
return true;
}
if (u.disabled && syncConfig.removeDisabled) {
return true;
}
return groups.filter(g => g.userMemberExternalIds.has(u.externalId)).length > 0;
});
const mainParts = filter.split("|");
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
return null;
}
protected forceGroup(force: boolean, users: UserEntry[]): boolean {
return force || (users != null && users.filter(u => !u.deleted && !u.disabled).length > 0);
const parts = mainParts[0].split(":");
if (parts.length !== 2) {
return null;
}
const keyword = parts[0].trim().toLowerCase();
let exclude = true;
if (keyword === "include") {
exclude = false;
} else if (keyword === "exclude") {
exclude = true;
} else {
return null;
}
const set = new Set<string>();
const pieces = parts[1].split(",");
for (const p of pieces) {
set.add(p.trim().toLowerCase());
}
return [exclude, set];
}
protected filterOutResult(setFilter: [boolean, Set<string>], result: string) {
if (setFilter != null) {
const cleanResult = result != null ? result.trim().toLowerCase() : "--";
const excluded = setFilter[0];
const set = setFilter[1];
if (excluded && set.has(cleanResult)) {
return true;
} else if (!excluded && !set.has(cleanResult)) {
return true;
}
}
return false;
}
protected filterUsersFromGroupsSet(
users: UserEntry[],
groups: GroupEntry[],
setFilter: [boolean, Set<string>],
syncConfig: SyncConfiguration
): UserEntry[] {
if (setFilter == null || users == null) {
return users;
}
return users.filter((u) => {
if (u.deleted) {
return true;
}
if (u.disabled && syncConfig.removeDisabled) {
return true;
}
return groups.filter((g) => g.userMemberExternalIds.has(u.externalId)).length > 0;
});
}
protected forceGroup(force: boolean, users: UserEntry[]): boolean {
return force || (users != null && users.filter((u) => !u.deleted && !u.disabled).length > 0);
}
}

View File

@@ -1,229 +0,0 @@
import { DirectoryType } from '../enums/directoryType';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { AzureConfiguration } from '../models/azureConfiguration';
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
import { LdapConfiguration } from '../models/ldapConfiguration';
import { OktaConfiguration } from '../models/oktaConfiguration';
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
const StoredSecurely = '[STORED SECURELY]';
const Keys = {
ldap: 'ldapPassword',
gsuite: 'gsuitePrivateKey',
azure: 'azureKey',
okta: 'oktaToken',
oneLogin: 'oneLoginClientSecret',
directoryConfigPrefix: 'directoryConfig_',
sync: 'syncConfig',
directoryType: 'directoryType',
userDelta: 'userDeltaToken',
groupDelta: 'groupDeltaToken',
lastUserSync: 'lastUserSync',
lastGroupSync: 'lastGroupSync',
lastSyncHash: 'lastSyncHash',
organizationId: 'organizationId',
};
export class ConfigurationService {
constructor(private storageService: StorageService, private secureStorageService: StorageService,
private useSecureStorageForSecrets = true) { }
async getDirectory<T>(type: DirectoryType): Promise<T> {
const config = await this.storageService.get<T>(Keys.directoryConfigPrefix + type);
if (config == null) {
return config;
}
if (this.useSecureStorageForSecrets) {
switch (type) {
case DirectoryType.Ldap:
(config as any).password = await this.secureStorageService.get<string>(Keys.ldap);
break;
case DirectoryType.AzureActiveDirectory:
(config as any).key = await this.secureStorageService.get<string>(Keys.azure);
break;
case DirectoryType.Okta:
(config as any).token = await this.secureStorageService.get<string>(Keys.okta);
break;
case DirectoryType.GSuite:
(config as any).privateKey = await this.secureStorageService.get<string>(Keys.gsuite);
break;
case DirectoryType.OneLogin:
(config as any).clientSecret = await this.secureStorageService.get<string>(Keys.oneLogin);
break;
}
}
return config;
}
async saveDirectory(type: DirectoryType,
config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration |
OneLoginConfiguration): Promise<any> {
const savedConfig: any = Object.assign({}, config);
if (this.useSecureStorageForSecrets) {
switch (type) {
case DirectoryType.Ldap:
if (savedConfig.password == null) {
await this.secureStorageService.remove(Keys.ldap);
} else {
await this.secureStorageService.save(Keys.ldap, savedConfig.password);
savedConfig.password = StoredSecurely;
}
break;
case DirectoryType.AzureActiveDirectory:
if (savedConfig.key == null) {
await this.secureStorageService.remove(Keys.azure);
} else {
await this.secureStorageService.save(Keys.azure, savedConfig.key);
savedConfig.key = StoredSecurely;
}
break;
case DirectoryType.Okta:
if (savedConfig.token == null) {
await this.secureStorageService.remove(Keys.okta);
} else {
await this.secureStorageService.save(Keys.okta, savedConfig.token);
savedConfig.token = StoredSecurely;
}
break;
case DirectoryType.GSuite:
if (savedConfig.privateKey == null) {
await this.secureStorageService.remove(Keys.gsuite);
} else {
(config as GSuiteConfiguration).privateKey = savedConfig.privateKey =
savedConfig.privateKey.replace(/\\n/g, '\n');
await this.secureStorageService.save(Keys.gsuite, savedConfig.privateKey);
savedConfig.privateKey = StoredSecurely;
}
break;
case DirectoryType.OneLogin:
if (savedConfig.clientSecret == null) {
await this.secureStorageService.remove(Keys.oneLogin);
} else {
await this.secureStorageService.save(Keys.oneLogin, savedConfig.clientSecret);
savedConfig.clientSecret = StoredSecurely;
}
break;
}
}
await this.storageService.save(Keys.directoryConfigPrefix + type, savedConfig);
}
getSync(): Promise<SyncConfiguration> {
return this.storageService.get<SyncConfiguration>(Keys.sync);
}
saveSync(config: SyncConfiguration) {
return this.storageService.save(Keys.sync, config);
}
getDirectoryType(): Promise<DirectoryType> {
return this.storageService.get<DirectoryType>(Keys.directoryType);
}
async saveDirectoryType(type: DirectoryType) {
const currentType = await this.getDirectoryType();
if (type !== currentType) {
await this.clearStatefulSettings();
}
return this.storageService.save(Keys.directoryType, type);
}
getUserDeltaToken(): Promise<string> {
return this.storageService.get<string>(Keys.userDelta);
}
saveUserDeltaToken(token: string) {
if (token == null) {
return this.storageService.remove(Keys.userDelta);
} else {
return this.storageService.save(Keys.userDelta, token);
}
}
getGroupDeltaToken(): Promise<string> {
return this.storageService.get<string>(Keys.groupDelta);
}
saveGroupDeltaToken(token: string) {
if (token == null) {
return this.storageService.remove(Keys.groupDelta);
} else {
return this.storageService.save(Keys.groupDelta, token);
}
}
async getLastUserSyncDate(): Promise<Date> {
const dateString = await this.storageService.get<string>(Keys.lastUserSync);
if (dateString == null) {
return null;
}
return new Date(dateString);
}
saveLastUserSyncDate(date: Date) {
if (date == null) {
return this.storageService.remove(Keys.lastUserSync);
} else {
return this.storageService.save(Keys.lastUserSync, date);
}
}
async getLastGroupSyncDate(): Promise<Date> {
const dateString = await this.storageService.get<string>(Keys.lastGroupSync);
if (dateString == null) {
return null;
}
return new Date(dateString);
}
saveLastGroupSyncDate(date: Date) {
if (date == null) {
return this.storageService.remove(Keys.lastGroupSync);
} else {
return this.storageService.save(Keys.lastGroupSync, date);
}
}
getLastSyncHash(): Promise<string> {
return this.storageService.get<string>(Keys.lastSyncHash);
}
saveLastSyncHash(hash: string) {
if (hash == null) {
return this.storageService.remove(Keys.lastSyncHash);
} else {
return this.storageService.save(Keys.lastSyncHash, hash);
}
}
getOrganizationId(): Promise<string> {
return this.storageService.get<string>(Keys.organizationId);
}
async saveOrganizationId(id: string) {
const currentId = await this.getOrganizationId();
if (currentId !== id) {
await this.clearStatefulSettings();
}
if (id == null) {
return this.storageService.remove(Keys.organizationId);
} else {
return this.storageService.save(Keys.organizationId, id);
}
}
async clearStatefulSettings(hashToo = false) {
await this.saveUserDeltaToken(null);
await this.saveGroupDeltaToken(null);
await this.saveLastGroupSyncDate(null);
await this.saveLastUserSyncDate(null);
if (hashToo) {
await this.saveLastSyncHash(null);
}
}
}

View File

@@ -1,6 +1,6 @@
import { GroupEntry } from '../models/groupEntry';
import { UserEntry } from '../models/userEntry';
import { GroupEntry } from "../models/groupEntry";
import { UserEntry } from "../models/userEntry";
export interface IDirectoryService {
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
}

View File

@@ -1,248 +1,263 @@
import { JWT } from 'google-auth-library';
import {
admin_directory_v1,
google,
} from 'googleapis';
import { JWT } from "google-auth-library";
import { admin_directory_v1, google } from "googleapis";
import { DirectoryType } from '../enums/directoryType';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { GroupEntry } from '../models/groupEntry';
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from "../models/groupEntry";
import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { BaseDirectoryService } from './baseDirectory.service';
import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service";
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
private client: JWT;
private service: admin_directory_v1.Admin;
private authParams: any;
private dirConfig: GSuiteConfiguration;
private syncConfig: SyncConfiguration;
private client: JWT;
private service: admin_directory_v1.Admin;
private authParams: any;
private dirConfig: GSuiteConfiguration;
private syncConfig: SyncConfiguration;
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) {
super();
this.service = google.admin('directory_v1');
constructor(
private logService: LogService,
private i18nService: I18nService,
private stateService: StateService
) {
super();
this.service = google.admin("directory_v1");
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.stateService.getDirectoryType();
if (type !== DirectoryType.GSuite) {
return;
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.GSuite) {
return;
}
this.dirConfig = await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite);
if (this.dirConfig == null) {
return;
}
this.syncConfig = await this.configurationService.getSync();
if (this.syncConfig == null) {
return;
}
await this.auth();
let users: UserEntry[] = [];
if (this.syncConfig.users) {
users = await this.getUsers();
}
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(setFilter, users);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
this.dirConfig = await this.stateService.getDirectory<GSuiteConfiguration>(
DirectoryType.GSuite
);
if (this.dirConfig == null) {
return;
}
private async getUsers(): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
let nextPageToken: string = null;
const filter = this.createCustomSet(this.syncConfig.userFilter);
while (true) {
this.logService.info('Querying users - nextPageToken:' + nextPageToken);
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
const res = await this.service.users.list(p);
if (res.status !== 200) {
throw new Error('User list API failed: ' + res.statusText);
}
nextPageToken = res.data.nextPageToken;
if (res.data.users != null) {
for (const user of res.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
}
const entry = this.buildUser(user, false);
if (entry != null) {
entries.push(entry);
}
}
}
if (nextPageToken == null) {
break;
}
}
nextPageToken = null;
while (true) {
this.logService.info('Querying deleted users - nextPageToken:' + nextPageToken);
const p = Object.assign({ showDeleted: true, query: query, pageToken: nextPageToken }, this.authParams);
const delRes = await this.service.users.list(p);
if (delRes.status !== 200) {
throw new Error('Deleted user list API failed: ' + delRes.statusText);
}
nextPageToken = delRes.data.nextPageToken;
if (delRes.data.users != null) {
for (const user of delRes.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
}
const entry = this.buildUser(user, true);
if (entry != null) {
entries.push(entry);
}
}
}
if (nextPageToken == null) {
break;
}
}
return entries;
this.syncConfig = await this.stateService.getSync();
if (this.syncConfig == null) {
return;
}
private buildUser(user: admin_directory_v1.Schema$User, deleted: boolean) {
if ((user.emails == null || user.emails === '') && !deleted) {
return null;
}
await this.auth();
const entry = new UserEntry();
entry.referenceId = user.id;
entry.externalId = user.id;
entry.email = user.primaryEmail != null ? user.primaryEmail.trim().toLowerCase() : null;
entry.disabled = user.suspended || false;
entry.deleted = deleted;
let users: UserEntry[] = [];
if (this.syncConfig.users) {
users = await this.getUsers();
}
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(setFilter, users);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
}
private async getUsers(): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
let nextPageToken: string = null;
const filter = this.createCustomSet(this.syncConfig.userFilter);
// eslint-disable-next-line
while (true) {
this.logService.info("Querying users - nextPageToken:" + nextPageToken);
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
const res = await this.service.users.list(p);
if (res.status !== 200) {
throw new Error("User list API failed: " + res.statusText);
}
nextPageToken = res.data.nextPageToken;
if (res.data.users != null) {
for (const user of res.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
}
const entry = this.buildUser(user, false);
if (entry != null) {
entries.push(entry);
}
}
}
if (nextPageToken == null) {
break;
}
}
nextPageToken = null;
// eslint-disable-next-line
while (true) {
this.logService.info("Querying deleted users - nextPageToken:" + nextPageToken);
const p = Object.assign(
{ showDeleted: true, query: query, pageToken: nextPageToken },
this.authParams
);
const delRes = await this.service.users.list(p);
if (delRes.status !== 200) {
throw new Error("Deleted user list API failed: " + delRes.statusText);
}
nextPageToken = delRes.data.nextPageToken;
if (delRes.data.users != null) {
for (const user of delRes.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
}
const entry = this.buildUser(user, true);
if (entry != null) {
entries.push(entry);
}
}
}
if (nextPageToken == null) {
break;
}
}
return entries;
}
private buildUser(user: admin_directory_v1.Schema$User, deleted: boolean) {
if ((user.emails == null || user.emails === "") && !deleted) {
return null;
}
const entry = new UserEntry();
entry.referenceId = user.id;
entry.externalId = user.id;
entry.email = user.primaryEmail != null ? user.primaryEmail.trim().toLowerCase() : null;
entry.disabled = user.suspended || false;
entry.deleted = deleted;
return entry;
}
private async getGroups(
setFilter: [boolean, Set<string>],
users: UserEntry[]
): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
let nextPageToken: string = null;
// eslint-disable-next-line
while (true) {
this.logService.info("Querying groups - nextPageToken:" + nextPageToken);
const p = Object.assign({ pageToken: nextPageToken }, this.authParams);
const res = await this.service.groups.list(p);
if (res.status !== 200) {
throw new Error("Group list API failed: " + res.statusText);
}
nextPageToken = res.data.nextPageToken;
if (res.data.groups != null) {
for (const group of res.data.groups) {
if (!this.filterOutResult(setFilter, group.name)) {
const entry = await this.buildGroup(group, users);
entries.push(entry);
}
}
}
if (nextPageToken == null) {
break;
}
}
return entries;
}
private async buildGroup(group: admin_directory_v1.Schema$Group, users: UserEntry[]) {
let nextPageToken: string = null;
const entry = new GroupEntry();
entry.referenceId = group.id;
entry.externalId = group.id;
entry.name = group.name;
// eslint-disable-next-line
while (true) {
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
const memRes = await this.service.members.list(p);
if (memRes.status !== 200) {
this.logService.warning("Group member list API failed: " + memRes.statusText);
return entry;
}
nextPageToken = memRes.data.nextPageToken;
if (memRes.data.members != null) {
for (const member of memRes.data.members) {
if (member.type == null) {
continue;
}
const type = member.type.toLowerCase();
if (type === "user") {
if (member.status == null || member.status.toLowerCase() !== "active") {
continue;
}
entry.userMemberExternalIds.add(member.id);
} else if (type === "group") {
entry.groupMemberReferenceIds.add(member.id);
} else if (type === "customer") {
for (const user of users) {
entry.userMemberExternalIds.add(user.externalId);
}
}
}
}
if (nextPageToken == null) {
break;
}
}
private async getGroups(setFilter: [boolean, Set<string>], users: UserEntry[]): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
let nextPageToken: string = null;
return entry;
}
while (true) {
this.logService.info('Querying groups - nextPageToken:' + nextPageToken);
const p = Object.assign({ pageToken: nextPageToken }, this.authParams);
const res = await this.service.groups.list(p);
if (res.status !== 200) {
throw new Error('Group list API failed: ' + res.statusText);
}
nextPageToken = res.data.nextPageToken;
if (res.data.groups != null) {
for (const group of res.data.groups) {
if (!this.filterOutResult(setFilter, group.name)) {
const entry = await this.buildGroup(group, users);
entries.push(entry);
}
}
}
if (nextPageToken == null) {
break;
}
}
return entries;
private async auth() {
if (
this.dirConfig.clientEmail == null ||
this.dirConfig.privateKey == null ||
this.dirConfig.adminUser == null ||
this.dirConfig.domain == null
) {
throw new Error(this.i18nService.t("dirConfigIncomplete"));
}
private async buildGroup(group: admin_directory_v1.Schema$Group, users: UserEntry[]) {
let nextPageToken: string = null;
this.client = new google.auth.JWT({
email: this.dirConfig.clientEmail,
key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null,
subject: this.dirConfig.adminUser,
scopes: [
"https://www.googleapis.com/auth/admin.directory.user.readonly",
"https://www.googleapis.com/auth/admin.directory.group.readonly",
"https://www.googleapis.com/auth/admin.directory.group.member.readonly",
],
});
const entry = new GroupEntry();
entry.referenceId = group.id;
entry.externalId = group.id;
entry.name = group.name;
await this.client.authorize();
while (true) {
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
const memRes = await this.service.members.list(p);
if (memRes.status !== 200) {
this.logService.warning('Group member list API failed: ' + memRes.statusText);
return entry;
}
nextPageToken = memRes.data.nextPageToken;
if (memRes.data.members != null) {
for (const member of memRes.data.members) {
if (member.type == null) {
continue;
}
const type = member.type.toLowerCase();
if (type === 'user') {
if (member.status == null || member.status.toLowerCase() !== 'active') {
continue;
}
entry.userMemberExternalIds.add(member.id);
} else if (type === 'group') {
entry.groupMemberReferenceIds.add(member.id);
} else if (type === 'customer') {
for (const user of users) {
entry.userMemberExternalIds.add(user.externalId);
}
}
}
}
if (nextPageToken == null) {
break;
}
}
return entry;
this.authParams = {
auth: this.client,
};
if (this.dirConfig.domain != null && this.dirConfig.domain.trim() !== "") {
this.authParams.domain = this.dirConfig.domain;
}
private async auth() {
if (this.dirConfig.clientEmail == null || this.dirConfig.privateKey == null ||
this.dirConfig.adminUser == null || this.dirConfig.domain == null) {
throw new Error(this.i18nService.t('dirConfigIncomplete'));
}
this.client = new google.auth.JWT({
email: this.dirConfig.clientEmail,
key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null,
subject: this.dirConfig.adminUser,
scopes: [
'https://www.googleapis.com/auth/admin.directory.user.readonly',
'https://www.googleapis.com/auth/admin.directory.group.readonly',
'https://www.googleapis.com/auth/admin.directory.group.member.readonly',
],
});
await this.client.authorize();
this.authParams = {
auth: this.client,
};
if (this.dirConfig.domain != null && this.dirConfig.domain.trim() !== '') {
this.authParams.domain = this.dirConfig.domain;
}
if (this.dirConfig.customer != null && this.dirConfig.customer.trim() !== '') {
this.authParams.customer = this.dirConfig.customer;
}
if (this.dirConfig.customer != null && this.dirConfig.customer.trim() !== "") {
this.authParams.customer = this.dirConfig.customer;
}
}
}

View File

@@ -1,15 +1,18 @@
import * as fs from 'fs';
import * as path from 'path';
import * as fs from "fs";
import * as path from "path";
import { I18nService as BaseI18nService } from 'jslib-common/services/i18n.service';
import { I18nService as BaseI18nService } from "jslib-common/services/i18n.service";
export class I18nService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) {
super(systemLanguage, localesDirectory, (formattedLocale: string) => {
const filePath = path.join(__dirname, this.localesDirectory + '/' + formattedLocale + '/messages.json');
const localesJson = fs.readFileSync(filePath, 'utf8');
const locales = JSON.parse(localesJson.replace(/^\uFEFF/, '')); // strip the BOM
return Promise.resolve(locales);
});
}
constructor(systemLanguage: string, localesDirectory: string) {
super(systemLanguage, localesDirectory, (formattedLocale: string) => {
const filePath = path.join(
__dirname,
this.localesDirectory + "/" + formattedLocale + "/messages.json"
);
const localesJson = fs.readFileSync(filePath, "utf8");
const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM
return Promise.resolve(locales);
});
}
}

View File

@@ -1,29 +1,25 @@
import {
deletePassword,
getPassword,
setPassword,
} from 'keytar';
import { deletePassword, getPassword, setPassword } from "keytar";
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { StorageService } from "jslib-common/abstractions/storage.service";
export class KeytarSecureStorageService implements StorageService {
constructor(private serviceName: string) { }
constructor(private serviceName: string) {}
get<T>(key: string): Promise<T> {
return getPassword(this.serviceName, key).then(val => {
return JSON.parse(val) as T;
});
}
get<T>(key: string): Promise<T> {
return getPassword(this.serviceName, key).then((val) => {
return JSON.parse(val) as T;
});
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) != null;
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) != null;
}
save(key: string, obj: any): Promise<any> {
return setPassword(this.serviceName, key, JSON.stringify(obj));
}
save(key: string, obj: any): Promise<any> {
return setPassword(this.serviceName, key, JSON.stringify(obj));
}
remove(key: string): Promise<any> {
return deletePassword(this.serviceName, key);
}
remove(key: string): Promise<any> {
return deletePassword(this.serviceName, key);
}
}

View File

@@ -1,448 +1,502 @@
import * as fs from 'fs';
import * as ldap from 'ldapjs';
import * as fs from "fs";
import { checkServerIdentity, PeerCertificate } from "tls";
import { checkServerIdentity, PeerCertificate } from 'tls';
import * as ldap from "ldapjs";
import { DirectoryType } from '../enums/directoryType';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { Utils } from "jslib-common/misc/utils";
import { GroupEntry } from '../models/groupEntry';
import { LdapConfiguration } from '../models/ldapConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from "../models/groupEntry";
import { LdapConfiguration } from "../models/ldapConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { Utils } from 'jslib-common/misc/utils';
import { IDirectoryService } from "./directory.service";
const UserControlAccountDisabled = 2;
export class LdapDirectoryService implements IDirectoryService {
private client: ldap.Client;
private dirConfig: LdapConfiguration;
private syncConfig: SyncConfiguration;
private client: ldap.Client;
private dirConfig: LdapConfiguration;
private syncConfig: SyncConfiguration;
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) { }
constructor(
private logService: LogService,
private i18nService: I18nService,
private stateService: StateService
) {}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.Ldap) {
return;
}
this.dirConfig = await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap);
if (this.dirConfig == null) {
return;
}
this.syncConfig = await this.configurationService.getSync();
if (this.syncConfig == null) {
return;
}
await this.bind();
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
}
let groups: GroupEntry[];
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);
}
await this.unbind();
return [groups, users];
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.stateService.getDirectoryType();
if (type !== DirectoryType.Ldap) {
return;
}
private async getUsers(force: boolean): Promise<UserEntry[]> {
const lastSync = await this.configurationService.getLastUserSyncDate();
let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter);
filter = this.buildRevisionFilter(filter, force, lastSync);
const path = this.makeSearchPath(this.syncConfig.userPath);
this.logService.info('User search: ' + path + ' => ' + filter);
const regularUsers = await this.search<UserEntry>(path, filter, (se: any) => this.buildUser(se, false));
if (!this.dirConfig.ad) {
return regularUsers;
}
try {
let deletedFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, '(isDeleted=TRUE)');
deletedFilter = this.buildRevisionFilter(deletedFilter, force, lastSync);
const deletedPath = this.makeSearchPath('CN=Deleted Objects');
this.logService.info('Deleted user search: ' + deletedPath + ' => ' + deletedFilter);
const delControl = new (ldap as any).Control({ type: '1.2.840.113556.1.4.417', criticality: true });
const deletedUsers = await this.search<UserEntry>(deletedPath, deletedFilter,
(se: any) => this.buildUser(se, true), [delControl]);
return regularUsers.concat(deletedUsers);
} catch (e) {
this.logService.warning('Cannot query deleted users.');
return regularUsers;
}
this.dirConfig = await this.stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap);
if (this.dirConfig == null) {
return;
}
private buildUser(searchEntry: any, deleted: boolean): UserEntry {
const user = new UserEntry();
user.referenceId = searchEntry.objectName;
user.deleted = deleted;
if (user.referenceId == null) {
return null;
}
user.externalId = this.getExternalId(searchEntry, user.referenceId);
user.disabled = this.entryDisabled(searchEntry);
user.email = this.getAttr(searchEntry, this.syncConfig.userEmailAttribute);
if (user.email == null && this.syncConfig.useEmailPrefixSuffix &&
this.syncConfig.emailPrefixAttribute != null && this.syncConfig.emailSuffix != null) {
const prefixAttr = this.getAttr(searchEntry, this.syncConfig.emailPrefixAttribute);
if (prefixAttr != null) {
user.email = prefixAttr + this.syncConfig.emailSuffix;
}
}
if (user.email != null) {
user.email = user.email.trim().toLowerCase();
}
if (!user.deleted && (user.email == null || user.email.trim() === '')) {
return null;
}
return user;
this.syncConfig = await this.stateService.getSync();
if (this.syncConfig == null) {
return;
}
private async getGroups(force: boolean): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
await this.bind();
const lastSync = await this.configurationService.getLastUserSyncDate();
const originalFilter = this.buildBaseFilter(this.syncConfig.groupObjectClass, this.syncConfig.groupFilter);
let filter = originalFilter;
const revisionFilter = this.buildRevisionFilter(filter, force, lastSync);
const searchSinceRevision = filter !== revisionFilter;
filter = revisionFilter;
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
}
const path = this.makeSearchPath(this.syncConfig.groupPath);
this.logService.info('Group search: ' + path + ' => ' + filter);
let groups: GroupEntry[];
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);
}
let groupSearchEntries: any[] = [];
const initialSearchGroupIds = await this.search<string>(path, filter, (se: any) => {
groupSearchEntries.push(se);
return se.objectName;
await this.unbind();
return [groups, users];
}
private async getUsers(force: boolean): Promise<UserEntry[]> {
const lastSync = await this.stateService.getLastUserSync();
let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter);
filter = this.buildRevisionFilter(filter, force, lastSync);
const path = this.makeSearchPath(this.syncConfig.userPath);
this.logService.info("User search: " + path + " => " + filter);
const regularUsers = await this.search<UserEntry>(path, filter, (se: any) =>
this.buildUser(se, false)
);
if (!this.dirConfig.ad) {
return regularUsers;
}
try {
let deletedFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, "(isDeleted=TRUE)");
deletedFilter = this.buildRevisionFilter(deletedFilter, force, lastSync);
const deletedPath = this.makeSearchPath("CN=Deleted Objects");
this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter);
const delControl = new (ldap as any).Control({
type: "1.2.840.113556.1.4.417",
criticality: true,
});
const deletedUsers = await this.search<UserEntry>(
deletedPath,
deletedFilter,
(se: any) => this.buildUser(se, true),
[delControl]
);
return regularUsers.concat(deletedUsers);
} catch (e) {
this.logService.warning("Cannot query deleted users.");
return regularUsers;
}
}
private buildUser(searchEntry: any, deleted: boolean): UserEntry {
const user = new UserEntry();
user.referenceId = searchEntry.objectName;
user.deleted = deleted;
if (user.referenceId == null) {
return null;
}
user.externalId = this.getExternalId(searchEntry, user.referenceId);
user.disabled = this.entryDisabled(searchEntry);
user.email = this.getAttr(searchEntry, this.syncConfig.userEmailAttribute);
if (
user.email == null &&
this.syncConfig.useEmailPrefixSuffix &&
this.syncConfig.emailPrefixAttribute != null &&
this.syncConfig.emailSuffix != null
) {
const prefixAttr = this.getAttr(searchEntry, this.syncConfig.emailPrefixAttribute);
if (prefixAttr != null) {
user.email = prefixAttr + this.syncConfig.emailSuffix;
}
}
if (user.email != null) {
user.email = user.email.trim().toLowerCase();
}
if (!user.deleted && (user.email == null || user.email.trim() === "")) {
return null;
}
return user;
}
private async getGroups(force: boolean): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
const lastSync = await this.stateService.getLastUserSync();
const originalFilter = this.buildBaseFilter(
this.syncConfig.groupObjectClass,
this.syncConfig.groupFilter
);
let filter = originalFilter;
const revisionFilter = this.buildRevisionFilter(filter, force, lastSync);
const searchSinceRevision = filter !== revisionFilter;
filter = revisionFilter;
const path = this.makeSearchPath(this.syncConfig.groupPath);
this.logService.info("Group search: " + path + " => " + filter);
let groupSearchEntries: any[] = [];
const initialSearchGroupIds = await this.search<string>(path, filter, (se: any) => {
groupSearchEntries.push(se);
return se.objectName;
});
if (searchSinceRevision && initialSearchGroupIds.length === 0) {
return [];
} else if (searchSinceRevision) {
groupSearchEntries = await this.search<string>(path, originalFilter, (se: any) => se);
}
const userFilter = this.buildBaseFilter(
this.syncConfig.userObjectClass,
this.syncConfig.userFilter
);
const userPath = this.makeSearchPath(this.syncConfig.userPath);
const userIdMap = new Map<string, string>();
await this.search<string>(userPath, userFilter, (se: any) => {
userIdMap.set(se.objectName, this.getExternalId(se, se.objectName));
return se;
});
for (const se of groupSearchEntries) {
const group = this.buildGroup(se, userIdMap);
if (group != null) {
entries.push(group);
}
}
return entries;
}
private buildGroup(searchEntry: any, userMap: Map<string, string>) {
const group = new GroupEntry();
group.referenceId = searchEntry.objectName;
if (group.referenceId == null) {
return null;
}
group.externalId = this.getExternalId(searchEntry, group.referenceId);
group.name = this.getAttr(searchEntry, this.syncConfig.groupNameAttribute);
if (group.name == null) {
group.name = this.getAttr(searchEntry, "cn");
}
if (group.name == null) {
return null;
}
const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute);
if (members != null) {
for (const memDn of members) {
if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) {
group.userMemberExternalIds.add(userMap.get(memDn));
} else if (!group.groupMemberReferenceIds.has(memDn)) {
group.groupMemberReferenceIds.add(memDn);
}
}
}
return group;
}
private getExternalId(searchEntry: any, referenceId: string) {
const attrObj = this.getAttrObj(searchEntry, "objectGUID");
if (attrObj != null && attrObj._vals != null && attrObj._vals.length > 0) {
return this.bufToGuid(attrObj._vals[0]);
} else {
return referenceId;
}
}
private buildBaseFilter(objectClass: string, subFilter: string): string {
let filter = this.buildObjectClassFilter(objectClass);
if (subFilter != null && subFilter.trim() !== "") {
filter = "(&" + filter + subFilter + ")";
}
return filter;
}
private buildObjectClassFilter(objectClass: string): string {
return "(&(objectClass=" + objectClass + "))";
}
private buildRevisionFilter(baseFilter: string, force: boolean, lastRevisionDate: Date) {
const revisionAttr = this.syncConfig.revisionDateAttribute;
if (!force && lastRevisionDate != null && revisionAttr != null && revisionAttr.trim() !== "") {
const dateString = lastRevisionDate.toISOString().replace(/[-:T]/g, "").substr(0, 16) + "Z";
baseFilter = "(&" + baseFilter + "(" + revisionAttr + ">=" + dateString + "))";
}
return baseFilter;
}
private makeSearchPath(pathPrefix: string) {
if (this.dirConfig.rootPath.toLowerCase().indexOf("dc=") === -1) {
return pathPrefix;
}
if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== "") {
const trimmedRootPath = this.dirConfig.rootPath.trim().toLowerCase();
let path = trimmedRootPath.substr(trimmedRootPath.indexOf("dc="));
if (pathPrefix != null && pathPrefix.trim() !== "") {
path = pathPrefix.trim() + "," + path;
}
return path;
}
return null;
}
private getAttrObj(searchEntry: any, attr: string): any {
if (searchEntry == null || searchEntry.attributes == null) {
return null;
}
const attrs = searchEntry.attributes.filter((a: any) => a.type === attr);
if (
attrs == null ||
attrs.length === 0 ||
attrs[0].vals == null ||
attrs[0].vals.length === 0
) {
return null;
}
return attrs[0];
}
private getAttrVals(searchEntry: any, attr: string): string[] {
const obj = this.getAttrObj(searchEntry, attr);
if (obj == null) {
return null;
}
return obj.vals;
}
private getAttr(searchEntry: any, attr: string): string {
const vals = this.getAttrVals(searchEntry, attr);
if (vals == null) {
return null;
}
return vals[0];
}
private entryDisabled(searchEntry: any): boolean {
const c = this.getAttr(searchEntry, "userAccountControl");
if (c != null) {
try {
const control = parseInt(c, null);
// tslint:disable-next-line
return (control & UserControlAccountDisabled) === UserControlAccountDisabled;
} catch (e) {
this.logService.error(e);
}
}
return false;
}
private async search<T>(
path: string,
filter: string,
processEntry: (searchEntry: any) => T,
controls: ldap.Control[] = []
): Promise<T[]> {
const options: ldap.SearchOptions = {
filter: filter,
scope: "sub",
paged: this.dirConfig.pagedSearch,
};
const entries: T[] = [];
return new Promise<T[]>((resolve, reject) => {
this.client.search(path, options, controls, (err, res) => {
if (err != null) {
reject(err);
return;
}
res.on("error", (resErr) => {
reject(resErr);
});
if (searchSinceRevision && initialSearchGroupIds.length === 0) {
return [];
} else if (searchSinceRevision) {
groupSearchEntries = await this.search<string>(path, originalFilter, (se: any) => se);
}
const userFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter);
const userPath = this.makeSearchPath(this.syncConfig.userPath);
const userIdMap = new Map<string, string>();
await this.search<string>(userPath, userFilter, (se: any) => {
userIdMap.set(se.objectName, this.getExternalId(se, se.objectName));
return se;
res.on("searchEntry", (entry) => {
const e = processEntry(entry);
if (e != null) {
entries.push(e);
}
});
for (const se of groupSearchEntries) {
const group = this.buildGroup(se, userIdMap);
if (group != null) {
entries.push(group);
}
res.on("end", (result) => {
resolve(entries);
});
});
});
}
private async bind(): Promise<any> {
return new Promise<void>((resolve, reject) => {
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
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 = {};
if (this.dirConfig.ssl) {
if (this.dirConfig.sslAllowUnauthorized) {
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
}
return entries;
}
private buildGroup(searchEntry: any, userMap: Map<string, string>) {
const group = new GroupEntry();
group.referenceId = searchEntry.objectName;
if (group.referenceId == null) {
return null;
}
group.externalId = this.getExternalId(searchEntry, group.referenceId);
group.name = this.getAttr(searchEntry, this.syncConfig.groupNameAttribute);
if (group.name == null) {
group.name = this.getAttr(searchEntry, 'cn');
}
if (group.name == null) {
return null;
}
const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute);
if (members != null) {
for (const memDn of members) {
if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) {
group.userMemberExternalIds.add(userMap.get(memDn));
} else if (!group.groupMemberReferenceIds.has(memDn)) {
group.groupMemberReferenceIds.add(memDn);
}
}
}
return group;
}
private getExternalId(searchEntry: any, referenceId: string) {
const attrObj = this.getAttrObj(searchEntry, 'objectGUID');
if (attrObj != null && attrObj._vals != null && attrObj._vals.length > 0) {
return this.bufToGuid(attrObj._vals[0]);
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 {
return referenceId;
if (
this.dirConfig.tlsCaPath != null &&
this.dirConfig.tlsCaPath !== "" &&
fs.existsSync(this.dirConfig.tlsCaPath)
) {
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
}
}
}
}
private buildBaseFilter(objectClass: string, subFilter: string): string {
let filter = this.buildObjectClassFilter(objectClass);
if (subFilter != null && subFilter.trim() !== '') {
filter = '(&' + filter + subFilter + ')';
}
return filter;
}
tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
options.tlsOptions = tlsOptions;
private buildObjectClassFilter(objectClass: string): string {
return '(&(objectClass=' + objectClass + '))';
}
this.client = ldap.createClient(options);
private buildRevisionFilter(baseFilter: string, force: boolean, lastRevisionDate: Date) {
const revisionAttr = this.syncConfig.revisionDateAttribute;
if (!force && lastRevisionDate != null && revisionAttr != null && revisionAttr.trim() !== '') {
const dateString = lastRevisionDate.toISOString().replace(/[-:T]/g, '').substr(0, 16) + 'Z';
baseFilter = '(&' + baseFilter + '(' + revisionAttr + '>=' + dateString + '))';
}
const user =
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;
return baseFilter;
}
if (user == null || pass == null) {
reject(this.i18nService.t("usernamePasswordNotConfigured"));
return;
}
private makeSearchPath(pathPrefix: string) {
if (this.dirConfig.rootPath.toLowerCase().indexOf('dc=') === -1) {
return pathPrefix;
}
if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== '') {
const trimmedRootPath = this.dirConfig.rootPath.trim().toLowerCase();
let path = trimmedRootPath.substr(trimmedRootPath.indexOf('dc='));
if (pathPrefix != null && pathPrefix.trim() !== '') {
path = pathPrefix.trim() + ',' + path;
}
return path;
}
return null;
}
private getAttrObj(searchEntry: any, attr: string): any {
if (searchEntry == null || searchEntry.attributes == null) {
return null;
}
const attrs = searchEntry.attributes.filter((a: any) => a.type === attr);
if (attrs == null || attrs.length === 0 || attrs[0].vals == null || attrs[0].vals.length === 0) {
return null;
}
return attrs[0];
}
private getAttrVals(searchEntry: any, attr: string): string[] {
const obj = this.getAttrObj(searchEntry, attr);
if (obj == null) {
return null;
}
return obj.vals;
}
private getAttr(searchEntry: any, attr: string): string {
const vals = this.getAttrVals(searchEntry, attr);
if (vals == null) {
return null;
}
return vals[0];
}
private entryDisabled(searchEntry: any): boolean {
const c = this.getAttr(searchEntry, 'userAccountControl');
if (c != null) {
try {
const control = parseInt(c, null);
// tslint:disable-next-line
return (control & UserControlAccountDisabled) === UserControlAccountDisabled;
} catch { }
}
return false;
}
private async search<T>(path: string, filter: string, processEntry: (searchEntry: any) => T,
controls: ldap.Control[] = []): Promise<T[]> {
const options: ldap.SearchOptions = {
filter: filter,
scope: 'sub',
paged: this.dirConfig.pagedSearch,
};
const entries: T[] = [];
return new Promise<T[]>((resolve, reject) => {
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);
});
if (this.dirConfig.startTls && this.dirConfig.ssl) {
this.client.starttls(options.tlsOptions, undefined, (err, res) => {
if (err != null) {
reject(err.message);
} else {
this.client.bind(user, pass, (err2) => {
if (err2 != null) {
reject(err2.message);
} else {
resolve();
}
});
}
});
}
private async bind(): Promise<any> {
return new Promise<void>((resolve, reject) => {
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
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 = {};
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;
options.tlsOptions = tlsOptions;
this.client = ldap.createClient(options);
const user = 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) {
reject(this.i18nService.t('usernamePasswordNotConfigured'));
return;
}
if (this.dirConfig.startTls && this.dirConfig.ssl) {
this.client.starttls(options.tlsOptions, undefined, (err, res) => {
if (err != null) {
reject(err.message);
} else {
this.client.bind(user, pass, err2 => {
if (err2 != null) {
reject(err2.message);
} else {
resolve();
}
});
}
});
} else {
this.client.bind(user, pass, err => {
if (err != null) {
reject(err.message);
} else {
resolve();
}
});
}
} else {
this.client.bind(user, pass, (err) => {
if (err != null) {
reject(err.message);
} else {
resolve();
}
});
}
}
});
}
private async unbind(): Promise<void> {
return new Promise((resolve, reject) => {
this.client.unbind(err => {
if (err != null) {
reject(err);
} else {
resolve();
}
});
});
}
private bufToGuid(buf: Buffer) {
const arr = new Uint8Array(buf);
const p1 = arr.slice(0, 4).reverse().buffer;
const p2 = arr.slice(4, 6).reverse().buffer;
const p3 = arr.slice(6, 8).reverse().buffer;
const p4 = arr.slice(8, 10).buffer;
const p5 = arr.slice(10).buffer;
const guid = Utils.fromBufferToHex(p1) + '-' + Utils.fromBufferToHex(p2) + '-' + Utils.fromBufferToHex(p3) +
'-' + Utils.fromBufferToHex(p4) + '-' + Utils.fromBufferToHex(p5);
return guid.toLowerCase();
}
private checkServerIdentityAltNames(host: string, cert: PeerCertificate) {
// 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)
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
// See https://github.com/nodejs/node/issues/11771 for details
if (cert && cert.subject == null && /(IP|DNS|URL)/.test(cert.subjectaltname)) {
cert.subject = {
C: null,
ST: null,
L: null,
O: null,
OU: null,
CN: null,
};
private async unbind(): Promise<void> {
return new Promise((resolve, reject) => {
this.client.unbind((err) => {
if (err != null) {
reject(err);
} else {
resolve();
}
});
});
}
return checkServerIdentity(host, cert);
private bufToGuid(buf: Buffer) {
const arr = new Uint8Array(buf);
const p1 = arr.slice(0, 4).reverse().buffer;
const p2 = arr.slice(4, 6).reverse().buffer;
const p3 = arr.slice(6, 8).reverse().buffer;
const p4 = arr.slice(8, 10).buffer;
const p5 = arr.slice(10).buffer;
const guid =
Utils.fromBufferToHex(p1) +
"-" +
Utils.fromBufferToHex(p2) +
"-" +
Utils.fromBufferToHex(p3) +
"-" +
Utils.fromBufferToHex(p4) +
"-" +
Utils.fromBufferToHex(p5);
return guid.toLowerCase();
}
private checkServerIdentityAltNames(host: string, cert: PeerCertificate) {
// 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)
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
// See https://github.com/nodejs/node/issues/11771 for details
if (cert && cert.subject == null && /(IP|DNS|URL)/.test(cert.subjectaltname)) {
cert.subject = {
C: null,
ST: null,
L: null,
O: null,
OU: null,
CN: null,
};
}
return checkServerIdentity(host, cert);
}
}

View File

@@ -1,28 +1,32 @@
import * as lock from 'proper-lockfile';
import * as lock from "proper-lockfile";
import { LogService } from 'jslib-common/abstractions/log.service';
import { LowdbStorageService as LowdbStorageServiceBase } from 'jslib-node/services/lowdbStorage.service';
import { Utils } from 'jslib-common/misc/utils';
import { LogService } from "jslib-common/abstractions/log.service";
import { Utils } from "jslib-common/misc/utils";
import { LowdbStorageService as LowdbStorageServiceBase } from "jslib-node/services/lowdbStorage.service";
export class LowdbStorageService extends LowdbStorageServiceBase {
constructor(logService: LogService, defaults?: any, dir?: string, allowCache = false, private requireLock = false) {
super(logService, defaults, dir, allowCache);
}
constructor(
logService: LogService,
defaults?: any,
dir?: string,
allowCache = false,
private requireLock = false
) {
super(logService, defaults, dir, allowCache);
}
protected async lockDbFile<T>(action: () => T): Promise<T> {
if (this.requireLock && !Utils.isNullOrWhitespace(this.dataFilePath)) {
this.logService.info('acquiring db file lock');
return await lock.lock(this.dataFilePath, { retries: 3 }).then(release => {
try {
return action();
} finally {
release();
}
});
} else {
return action();
protected async lockDbFile<T>(action: () => T): Promise<T> {
if (this.requireLock && !Utils.isNullOrWhitespace(this.dataFilePath)) {
this.logService.info("acquiring db file lock");
return await lock.lock(this.dataFilePath, { retries: 3 }).then((release) => {
try {
return action();
} finally {
release();
}
});
} else {
return action();
}
}
}

View File

@@ -1,17 +0,0 @@
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { NodeApiService as NodeApiServiceBase } from 'jslib-node/services/nodeApi.service';
export class NodeApiService extends NodeApiServiceBase {
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
private refreshTokenCallback: () => Promise<void>, logoutCallback: (expired: boolean) => Promise<void>,
customUserAgent: string = null, apiKeyRefresh: (clientId: string, clientSecret: string) => Promise<any>) {
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent, apiKeyRefresh);
}
doRefreshToken(): Promise<void> {
return this.refreshTokenCallback();
}
}

View File

@@ -0,0 +1,40 @@
import {
TwoFactorProviderDetails,
TwoFactorService,
} from "jslib-common/abstractions/twoFactor.service";
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { IdentityTwoFactorResponse } from "jslib-common/models/response/identityTwoFactorResponse";
export class NoopTwoFactorService implements TwoFactorService {
init() {
// Noop
}
getSupportedProviders(win: Window): TwoFactorProviderDetails[] {
return null;
}
getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType {
return null;
}
setSelectedProvider(type: TwoFactorProviderType) {
// Noop
}
clearSelectedProvider() {
// Noop
}
setProviders(response: IdentityTwoFactorResponse) {
// Noop
}
clearProviders() {
// Noop
}
getProviders(): Map<TwoFactorProviderType, { [key: string]: string }> {
return null;
}
}

View File

@@ -1,253 +1,269 @@
import { DirectoryType } from '../enums/directoryType';
import * as https from "https";
import { GroupEntry } from '../models/groupEntry';
import { OktaConfiguration } from '../models/oktaConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { BaseDirectoryService } from './baseDirectory.service';
import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from "../models/groupEntry";
import { OktaConfiguration } from "../models/oktaConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import * as https from 'https';
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service";
const DelayBetweenBuildGroupCallsInMilliseconds = 500;
export class OktaDirectoryService extends BaseDirectoryService implements IDirectoryService {
private dirConfig: OktaConfiguration;
private syncConfig: SyncConfiguration;
private lastBuildGroupCall: number;
private dirConfig: OktaConfiguration;
private syncConfig: SyncConfiguration;
private lastBuildGroupCall: number;
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) {
super();
constructor(
private logService: LogService,
private i18nService: I18nService,
private stateService: StateService
) {
super();
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.stateService.getDirectoryType();
if (type !== DirectoryType.Okta) {
return;
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.Okta) {
return;
}
this.dirConfig = await this.configurationService.getDirectory<OktaConfiguration>(DirectoryType.Okta);
if (this.dirConfig == null) {
return;
}
this.syncConfig = await this.configurationService.getSync();
if (this.syncConfig == null) {
return;
}
if (this.dirConfig.orgUrl == null || this.dirConfig.token == null) {
throw new Error(this.i18nService.t('dirConfigIncomplete'));
}
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
}
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
this.dirConfig = await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta);
if (this.dirConfig == null) {
return;
}
private async getUsers(force: boolean): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const lastSync = await this.configurationService.getLastUserSyncDate();
const oktaFilter = this.buildOktaFilter(this.syncConfig.userFilter, force, lastSync);
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
this.syncConfig = await this.stateService.getSync();
if (this.syncConfig == null) {
return;
}
this.logService.info('Querying users.');
const usersPromise = this.apiGetMany('users?filter=' + this.encodeUrlParameter(oktaFilter))
.then((users: any[]) => {
for (const user of users) {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
if (this.dirConfig.orgUrl == null || this.dirConfig.token == null) {
throw new Error(this.i18nService.t("dirConfigIncomplete"));
}
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
}
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
}
private async getUsers(force: boolean): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const lastSync = await this.stateService.getLastUserSync();
const oktaFilter = this.buildOktaFilter(this.syncConfig.userFilter, force, lastSync);
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
this.logService.info("Querying users.");
const usersPromise = this.apiGetMany(
"users?filter=" + this.encodeUrlParameter(oktaFilter)
).then((users: any[]) => {
for (const user of users) {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
}
});
// Deactivated users have to be queried for separately, only when no filter is provided in the first query
let deactUsersPromise: any;
if (oktaFilter == null || oktaFilter.indexOf("lastUpdated ") === -1) {
let deactOktaFilter = 'status eq "DEPROVISIONED"';
if (oktaFilter != null) {
deactOktaFilter = "(" + oktaFilter + ") and " + deactOktaFilter;
}
deactUsersPromise = this.apiGetMany(
"users?filter=" + this.encodeUrlParameter(deactOktaFilter)
).then((users: any[]) => {
for (const user of users) {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
}
});
} else {
deactUsersPromise = Promise.resolve();
}
await Promise.all([usersPromise, deactUsersPromise]);
return entries;
}
private buildUser(user: any) {
const entry = new UserEntry();
entry.externalId = user.id;
entry.referenceId = user.id;
entry.email = user.profile.email != null ? user.profile.email.trim().toLowerCase() : null;
entry.deleted = user.status === "DEPROVISIONED";
entry.disabled = user.status === "SUSPENDED";
return entry;
}
private async getGroups(
force: boolean,
setFilter: [boolean, Set<string>]
): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
const lastSync = await this.stateService.getLastGroupSync();
const oktaFilter = this.buildOktaFilter(this.syncConfig.groupFilter, force, lastSync);
this.logService.info("Querying groups.");
await this.apiGetMany("groups?filter=" + this.encodeUrlParameter(oktaFilter)).then(
async (groups: any[]) => {
for (const group of groups.filter(
(g) => !this.filterOutResult(setFilter, g.profile.name)
)) {
const entry = await this.buildGroup(group);
if (entry != null) {
entries.push(entry);
}
}
}
);
return entries;
}
private async buildGroup(group: any): Promise<GroupEntry> {
const entry = new GroupEntry();
entry.externalId = group.id;
entry.referenceId = group.id;
entry.name = group.profile.name;
// throttle some to avoid rate limiting
const neededDelay =
DelayBetweenBuildGroupCallsInMilliseconds - (Date.now() - this.lastBuildGroupCall);
if (neededDelay > 0) {
await new Promise((resolve) => setTimeout(resolve, neededDelay));
}
this.lastBuildGroupCall = Date.now();
await this.apiGetMany("groups/" + group.id + "/users").then((users: any[]) => {
for (const user of users) {
entry.userMemberExternalIds.add(user.id);
}
});
return entry;
}
private buildOktaFilter(baseFilter: string, force: boolean, lastSync: Date) {
baseFilter = this.createDirectoryQuery(baseFilter);
baseFilter = baseFilter == null || baseFilter.trim() === "" ? null : baseFilter;
if (force || lastSync == null) {
return baseFilter;
}
const updatedFilter = 'lastUpdated gt "' + lastSync.toISOString() + '"';
if (baseFilter == null) {
return updatedFilter;
}
return "(" + baseFilter + ") and " + updatedFilter;
}
private encodeUrlParameter(filter: string): string {
return filter == null ? "" : encodeURIComponent(filter);
}
private async apiGetCall(url: string): Promise<[any, Map<string, string | string[]>]> {
const u = new URL(url);
return new Promise((resolve) => {
https.get(
{
hostname: u.hostname,
path: u.pathname + u.search,
port: 443,
headers: {
Authorization: "SSWS " + this.dirConfig.token,
Accept: "application/json",
},
},
(res) => {
let body = "";
res.on("data", (chunk) => {
body += chunk;
});
res.on("end", () => {
if (res.statusCode !== 200) {
resolve(null);
return;
}
const responseJson = JSON.parse(body);
if (res.headers != null) {
const headersMap = new Map<string, string | string[]>();
for (const key in res.headers) {
// eslint-disable-next-line
if (res.headers.hasOwnProperty(key)) {
const val = res.headers[key];
headersMap.set(key.toLowerCase(), val);
}
});
// Deactivated users have to be queried for separately, only when no filter is provided in the first query
let deactUsersPromise: any;
if (oktaFilter == null || oktaFilter.indexOf('lastUpdated ') === -1) {
let deactOktaFilter = 'status eq "DEPROVISIONED"';
if (oktaFilter != null) {
deactOktaFilter = '(' + oktaFilter + ') and ' + deactOktaFilter;
}
resolve([responseJson, headersMap]);
return;
}
deactUsersPromise = this.apiGetMany('users?filter=' + this.encodeUrlParameter(deactOktaFilter))
.then((users: any[]) => {
for (const user of users) {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
}
});
} else {
deactUsersPromise = Promise.resolve();
}
resolve([responseJson, null]);
});
await Promise.all([usersPromise, deactUsersPromise]);
return entries;
res.on("error", () => {
resolve(null);
});
}
);
});
}
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
const url =
endpoint.indexOf("https://") === 0 ? endpoint : `${this.dirConfig.orgUrl}/api/v1/${endpoint}`;
const response = await this.apiGetCall(url);
if (response == null || response[0] == null || !Array.isArray(response[0])) {
throw new Error("API call failed.");
}
private buildUser(user: any) {
const entry = new UserEntry();
entry.externalId = user.id;
entry.referenceId = user.id;
entry.email = user.profile.email != null ? user.profile.email.trim().toLowerCase() : null;
entry.deleted = user.status === 'DEPROVISIONED';
entry.disabled = user.status === 'SUSPENDED';
return entry;
if (response[0].length === 0) {
return currentData;
}
private async getGroups(force: boolean, setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
const lastSync = await this.configurationService.getLastGroupSyncDate();
const oktaFilter = this.buildOktaFilter(this.syncConfig.groupFilter, force, lastSync);
this.logService.info('Querying groups.');
await this.apiGetMany('groups?filter=' + this.encodeUrlParameter(oktaFilter)).then(async (groups: any[]) => {
for (const group of groups.filter(g => !this.filterOutResult(setFilter, g.profile.name))) {
const entry = await this.buildGroup(group);
if (entry != null) {
entries.push(entry);
}
}
});
return entries;
currentData = currentData.concat(response[0]);
if (response[1] == null) {
return currentData;
}
private async buildGroup(group: any): Promise<GroupEntry> {
const entry = new GroupEntry();
entry.externalId = group.id;
entry.referenceId = group.id;
entry.name = group.profile.name;
// throttle some to avoid rate limiting
const neededDelay = DelayBetweenBuildGroupCallsInMilliseconds - (Date.now() - this.lastBuildGroupCall);
if (neededDelay > 0) {
await new Promise(resolve =>
setTimeout(resolve, neededDelay));
}
this.lastBuildGroupCall = Date.now();
await this.apiGetMany('groups/' + group.id + '/users').then((users: any[]) => {
for (const user of users) {
entry.userMemberExternalIds.add(user.id);
}
});
return entry;
const linkHeader = response[1].get("link");
if (linkHeader == null || Array.isArray(linkHeader)) {
return currentData;
}
private buildOktaFilter(baseFilter: string, force: boolean, lastSync: Date) {
baseFilter = this.createDirectoryQuery(baseFilter);
baseFilter = baseFilter == null || baseFilter.trim() === '' ? null : baseFilter;
if (force || lastSync == null) {
return baseFilter;
let nextLink: string = null;
const linkHeaderParts = linkHeader.split(",");
for (const part of linkHeaderParts) {
if (part.indexOf('; rel="next"') > -1) {
const subParts = part.split(";");
if (subParts.length > 0 && subParts[0].indexOf("https://") > -1) {
nextLink = subParts[0].replace(">", "").replace("<", "").trim();
break;
}
const updatedFilter = 'lastUpdated gt "' + lastSync.toISOString() + '"';
if (baseFilter == null) {
return updatedFilter;
}
return '(' + baseFilter + ') and ' + updatedFilter;
}
}
private encodeUrlParameter(filter: string): string {
return filter == null ? '' : encodeURIComponent(filter);
}
private async apiGetCall(url: string): Promise<[any, Map<string, string | string[]>]> {
const u = new URL(url);
return new Promise(resolve => {
https.get({
hostname: u.hostname,
path: u.pathname + u.search,
port: 443,
headers: {
Authorization: 'SSWS ' + this.dirConfig.token,
Accept: 'application/json',
},
}, res => {
let body = '';
res.on('data', chunk => {
body += chunk;
});
res.on('end', () => {
if (res.statusCode !== 200) {
resolve(null);
return;
}
const responseJson = JSON.parse(body);
if (res.headers != null) {
const headersMap = new Map<string, string | string[]>();
for (const key in res.headers) {
if (res.headers.hasOwnProperty(key)) {
const val = res.headers[key];
headersMap.set(key.toLowerCase(), val);
}
}
resolve([responseJson, headersMap]);
return;
}
resolve([responseJson, null]);
});
res.on('error', () => {
resolve(null);
});
});
});
}
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
const url = endpoint.indexOf('https://') === 0 ? endpoint : `${this.dirConfig.orgUrl}/api/v1/${endpoint}`;
const response = await this.apiGetCall(url);
if (response == null || response[0] == null || !Array.isArray(response[0])) {
throw new Error('API call failed.');
}
if (response[0].length === 0) {
return currentData;
}
currentData = currentData.concat(response[0]);
if (response[1] == null) {
return currentData;
}
const linkHeader = response[1].get('link');
if (linkHeader == null || Array.isArray(linkHeader)) {
return currentData;
}
let nextLink: string = null;
const linkHeaderParts = linkHeader.split(',');
for (const part of linkHeaderParts) {
if (part.indexOf('; rel="next"') > -1) {
const subParts = part.split(';');
if (subParts.length > 0 && subParts[0].indexOf('https://') > -1) {
nextLink = subParts[0].replace('>', '').replace('<', '').trim();
break;
}
}
}
if (nextLink == null) {
return currentData;
}
return this.apiGetMany(nextLink, currentData);
if (nextLink == null) {
return currentData;
}
return this.apiGetMany(nextLink, currentData);
}
}

View File

@@ -1,195 +1,208 @@
import { DirectoryType } from '../enums/directoryType';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { GroupEntry } from '../models/groupEntry';
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from "../models/groupEntry";
import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { BaseDirectoryService } from './baseDirectory.service';
import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service";
// Basic email validation: something@something.something
const ValidEmailRegex = /^\S+@\S+\.\S+$/;
export class OneLoginDirectoryService extends BaseDirectoryService implements IDirectoryService {
private dirConfig: OneLoginConfiguration;
private syncConfig: SyncConfiguration;
private accessToken: string;
private allUsers: any[] = [];
private dirConfig: OneLoginConfiguration;
private syncConfig: SyncConfiguration;
private accessToken: string;
private allUsers: any[] = [];
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) {
super();
constructor(
private logService: LogService,
private i18nService: I18nService,
private stateService: StateService
) {
super();
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.stateService.getDirectoryType();
if (type !== DirectoryType.OneLogin) {
return;
}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.OneLogin) {
return;
}
this.dirConfig = await this.configurationService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin);
if (this.dirConfig == null) {
return;
}
this.syncConfig = await this.configurationService.getSync();
if (this.syncConfig == null) {
return;
}
if (this.dirConfig.clientId == null || this.dirConfig.clientSecret == null) {
throw new Error(this.i18nService.t('dirConfigIncomplete'));
}
this.accessToken = await this.getAccessToken();
if (this.accessToken == null) {
throw new Error('Could not get access token');
}
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
}
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
this.dirConfig = await this.stateService.getDirectory<OneLoginConfiguration>(
DirectoryType.OneLogin
);
if (this.dirConfig == null) {
return;
}
private async getUsers(force: boolean): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
this.logService.info('Querying users.');
this.allUsers = await this.apiGetMany('users' + (query != null ? '?' + query : ''));
this.allUsers.forEach(user => {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
});
return Promise.resolve(entries);
this.syncConfig = await this.stateService.getSync();
if (this.syncConfig == null) {
return;
}
private buildUser(user: any) {
const entry = new UserEntry();
entry.externalId = user.id;
entry.referenceId = user.id;
entry.deleted = false;
entry.disabled = user.status === 2;
entry.email = user.email;
if (!this.validEmailAddress(entry.email) && user.username != null && user.username !== '') {
if (this.validEmailAddress(user.username)) {
entry.email = user.username;
} else if (this.syncConfig.useEmailPrefixSuffix && this.syncConfig.emailSuffix != null) {
entry.email = user.username + this.syncConfig.emailSuffix;
}
}
if (entry.email != null) {
entry.email = entry.email.trim().toLowerCase();
}
if (!this.validEmailAddress(entry.email)) {
return null;
}
return entry;
if (this.dirConfig.clientId == null || this.dirConfig.clientSecret == null) {
throw new Error(this.i18nService.t("dirConfigIncomplete"));
}
private async getGroups(force: boolean, setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.groupFilter);
this.logService.info('Querying groups.');
const roles = await this.apiGetMany('roles' + (query != null ? '?' + query : ''));
roles.forEach(role => {
const entry = this.buildGroup(role);
if (entry != null && !this.filterOutResult(setFilter, entry.name)) {
entries.push(entry);
}
});
return Promise.resolve(entries);
this.accessToken = await this.getAccessToken();
if (this.accessToken == null) {
throw new Error("Could not get access token");
}
private buildGroup(group: any) {
const entry = new GroupEntry();
entry.externalId = group.id;
entry.referenceId = group.id;
entry.name = group.name;
if (this.allUsers != null) {
this.allUsers.forEach(user => {
if (user.role_id != null && user.role_id.indexOf(entry.referenceId) > -1) {
entry.userMemberExternalIds.add(user.id);
}
});
}
return entry;
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
}
private async getAccessToken() {
const response = await fetch(`https://api.${this.dirConfig.region}.onelogin.com/auth/oauth2/v2/token`, {
method: 'POST',
headers: new Headers({
'Authorization': 'Basic ' + btoa(this.dirConfig.clientId + ':' + this.dirConfig.clientSecret),
'Content-Type': 'application/json; charset=utf-8',
'Accept': 'application/json',
}),
body: JSON.stringify({
grant_type: 'client_credentials',
}),
});
if (response.status === 200) {
const responseJson = await response.json();
if (responseJson.access_token != null) {
return responseJson.access_token;
}
}
return null;
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(this.forceGroup(force, users), setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
private async apiGetCall(url: string): Promise<any> {
const req: RequestInit = {
method: 'GET',
headers: new Headers({
Authorization: 'bearer:' + this.accessToken,
Accept: 'application/json',
}),
};
const response = await fetch(new Request(url, req));
if (response.status === 200) {
const responseJson = await response.json();
return responseJson;
return [groups, users];
}
private async getUsers(force: boolean): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
this.logService.info("Querying users.");
this.allUsers = await this.apiGetMany("users" + (query != null ? "?" + query : ""));
this.allUsers.forEach((user) => {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
});
return Promise.resolve(entries);
}
private buildUser(user: any) {
const entry = new UserEntry();
entry.externalId = user.id;
entry.referenceId = user.id;
entry.deleted = false;
entry.disabled = user.status === 2;
entry.email = user.email;
if (!this.validEmailAddress(entry.email) && user.username != null && user.username !== "") {
if (this.validEmailAddress(user.username)) {
entry.email = user.username;
} else if (this.syncConfig.useEmailPrefixSuffix && this.syncConfig.emailSuffix != null) {
entry.email = user.username + this.syncConfig.emailSuffix;
}
}
if (entry.email != null) {
entry.email = entry.email.trim().toLowerCase();
}
if (!this.validEmailAddress(entry.email)) {
return null;
}
return entry;
}
private async getGroups(
force: boolean,
setFilter: [boolean, Set<string>]
): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.groupFilter);
this.logService.info("Querying groups.");
const roles = await this.apiGetMany("roles" + (query != null ? "?" + query : ""));
roles.forEach((role) => {
const entry = this.buildGroup(role);
if (entry != null && !this.filterOutResult(setFilter, entry.name)) {
entries.push(entry);
}
});
return Promise.resolve(entries);
}
private buildGroup(group: any) {
const entry = new GroupEntry();
entry.externalId = group.id;
entry.referenceId = group.id;
entry.name = group.name;
if (this.allUsers != null) {
this.allUsers.forEach((user) => {
if (user.role_id != null && user.role_id.indexOf(entry.referenceId) > -1) {
entry.userMemberExternalIds.add(user.id);
}
return null;
});
}
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
const url = endpoint.indexOf('https://') === 0 ? endpoint :
`https://api.${this.dirConfig.region}.onelogin.com/api/1/${endpoint}`;
const response = await this.apiGetCall(url);
if (response == null || response.status == null || response.data == null) {
return currentData;
}
if (response.status.code !== 200) {
throw new Error('API call failed.');
}
currentData = currentData.concat(response.data);
if (response.pagination == null || response.pagination.next_link == null) {
return currentData;
}
return this.apiGetMany(response.pagination.next_link, currentData);
}
return entry;
}
private validEmailAddress(email: string) {
return email != null && email !== '' && ValidEmailRegex.test(email);
private async getAccessToken() {
const response = await fetch(
`https://api.${this.dirConfig.region}.onelogin.com/auth/oauth2/v2/token`,
{
method: "POST",
headers: new Headers({
Authorization:
"Basic " + btoa(this.dirConfig.clientId + ":" + this.dirConfig.clientSecret),
"Content-Type": "application/json; charset=utf-8",
Accept: "application/json",
}),
body: JSON.stringify({
grant_type: "client_credentials",
}),
}
);
if (response.status === 200) {
const responseJson = await response.json();
if (responseJson.access_token != null) {
return responseJson.access_token;
}
}
return null;
}
private async apiGetCall(url: string): Promise<any> {
const req: RequestInit = {
method: "GET",
headers: new Headers({
Authorization: "bearer:" + this.accessToken,
Accept: "application/json",
}),
};
const response = await fetch(new Request(url, req));
if (response.status === 200) {
const responseJson = await response.json();
return responseJson;
}
return null;
}
private async apiGetMany(endpoint: string, currentData: any[] = []): Promise<any[]> {
const url =
endpoint.indexOf("https://") === 0
? endpoint
: `https://api.${this.dirConfig.region}.onelogin.com/api/1/${endpoint}`;
const response = await this.apiGetCall(url);
if (response == null || response.status == null || response.data == null) {
return currentData;
}
if (response.status.code !== 200) {
throw new Error("API call failed.");
}
currentData = currentData.concat(response.data);
if (response.pagination == null || response.pagination.next_link == null) {
return currentData;
}
return this.apiGetMany(response.pagination.next_link, currentData);
}
private validEmailAddress(email: string) {
return email != null && email !== "" && ValidEmailRegex.test(email);
}
}

View File

@@ -0,0 +1,581 @@
import { LogService } from "jslib-common/abstractions/log.service";
import { StateMigrationService } from "jslib-common/abstractions/stateMigration.service";
import { StorageService } from "jslib-common/abstractions/storage.service";
import { StateFactory } from "jslib-common/factories/stateFactory";
import { GlobalState } from "jslib-common/models/domain/globalState";
import { StorageOptions } from "jslib-common/models/domain/storageOptions";
import { StateService as BaseStateService } from "jslib-common/services/state.service";
import { StateService as StateServiceAbstraction } from "src/abstractions/state.service";
import { DirectoryType } from "src/enums/directoryType";
import { IConfiguration } from "src/models/IConfiguration";
import { Account } from "src/models/account";
import { AzureConfiguration } from "src/models/azureConfiguration";
import { GSuiteConfiguration } from "src/models/gsuiteConfiguration";
import { LdapConfiguration } from "src/models/ldapConfiguration";
import { OktaConfiguration } from "src/models/oktaConfiguration";
import { OneLoginConfiguration } from "src/models/oneLoginConfiguration";
import { SyncConfiguration } from "src/models/syncConfiguration";
const SecureStorageKeys = {
ldap: "ldapPassword",
gsuite: "gsuitePrivateKey",
azure: "azureKey",
okta: "oktaToken",
oneLogin: "oneLoginClientSecret",
userDelta: "userDeltaToken",
groupDelta: "groupDeltaToken",
lastUserSync: "lastUserSync",
lastGroupSync: "lastGroupSync",
lastSyncHash: "lastSyncHash",
};
const keys = {
tempAccountSettings: "tempAccountSettings",
tempDirectoryConfigs: "tempDirectoryConfigs",
tempDirectorySettings: "tempDirectorySettings",
};
const StoredSecurely = "[STORED SECURELY]";
export class StateService
extends BaseStateService<GlobalState, Account>
implements StateServiceAbstraction
{
constructor(
protected storageService: StorageService,
protected secureStorageService: StorageService,
protected logService: LogService,
protected stateMigrationService: StateMigrationService,
private useSecureStorageForSecrets = true,
protected stateFactory: StateFactory<GlobalState, Account>
) {
super(storageService, secureStorageService, logService, stateMigrationService, stateFactory);
}
async getDirectory<T extends IConfiguration>(type: DirectoryType): Promise<T> {
const config = await this.getConfiguration(type);
if (config == null) {
return config as T;
}
if (this.useSecureStorageForSecrets) {
switch (type) {
case DirectoryType.Ldap:
(config as any).password = await this.getLdapKey();
break;
case DirectoryType.AzureActiveDirectory:
(config as any).key = await this.getAzureKey();
break;
case DirectoryType.Okta:
(config as any).token = await this.getOktaKey();
break;
case DirectoryType.GSuite:
(config as any).privateKey = await this.getGsuiteKey();
break;
case DirectoryType.OneLogin:
(config as any).clientSecret = await this.getOneLoginKey();
break;
}
}
return config as T;
}
async setDirectory(
type: DirectoryType,
config:
| LdapConfiguration
| GSuiteConfiguration
| AzureConfiguration
| OktaConfiguration
| OneLoginConfiguration
): Promise<any> {
const savedConfig: any = Object.assign({}, config);
if (this.useSecureStorageForSecrets) {
switch (type) {
case DirectoryType.Ldap:
await this.setLdapKey(savedConfig.password);
savedConfig.password = StoredSecurely;
await this.setLdapConfiguration(savedConfig);
break;
case DirectoryType.AzureActiveDirectory:
await this.setAzureKey(savedConfig.key);
savedConfig.key = StoredSecurely;
await this.setAzureConfiguration(savedConfig);
break;
case DirectoryType.Okta:
await this.setOktaKey(savedConfig.token);
savedConfig.token = StoredSecurely;
await this.setOktaConfiguration(savedConfig);
break;
case DirectoryType.GSuite:
if (savedConfig.privateKey == null) {
await this.setGsuiteKey(null);
} else {
(config as GSuiteConfiguration).privateKey = savedConfig.privateKey =
savedConfig.privateKey.replace(/\\n/g, "\n");
await this.setGsuiteKey(savedConfig.privateKey);
savedConfig.privateKey = StoredSecurely;
}
await this.setGsuiteConfiguration(savedConfig);
break;
case DirectoryType.OneLogin:
await this.setOneLoginKey(savedConfig.clientSecret);
savedConfig.clientSecret = StoredSecurely;
await this.setOneLoginConfiguration(savedConfig);
break;
}
}
}
async getLdapKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.ldap}`
);
}
async setLdapKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.ldap}`,
value,
options
);
}
async getGsuiteKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.gsuite}`
);
}
async setGsuiteKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.gsuite}`,
value,
options
);
}
async getAzureKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.azure}`
);
}
async setAzureKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.azure}`,
value,
options
);
}
async getOktaKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.okta}`
);
}
async setOktaKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.okta}`,
value,
options
);
}
async getOneLoginKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.oneLogin}`
);
}
async setOneLoginKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.oneLogin}`,
value,
options
);
}
async getUserDelta(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.userDelta}`
);
}
async setUserDelta(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.userDelta}`,
value,
options
);
}
async getGroupDelta(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.groupDelta}`
);
}
async setGroupDelta(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.groupDelta}`,
value,
options
);
}
async getConfiguration(type: DirectoryType): Promise<IConfiguration> {
switch (type) {
case DirectoryType.Ldap:
return await this.getLdapConfiguration();
case DirectoryType.GSuite:
return await this.getGsuiteConfiguration();
case DirectoryType.AzureActiveDirectory:
return await this.getAzureConfiguration();
case DirectoryType.Okta:
return await this.getOktaConfiguration();
case DirectoryType.OneLogin:
return await this.getOneLoginConfiguration();
}
}
async getLdapConfiguration(options?: StorageOptions): Promise<LdapConfiguration> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.ldap;
}
async setLdapConfiguration(value: LdapConfiguration, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.directoryConfigurations.ldap = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getGsuiteConfiguration(options?: StorageOptions): Promise<GSuiteConfiguration> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.gsuite;
}
async setGsuiteConfiguration(
value: GSuiteConfiguration,
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.directoryConfigurations.gsuite = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getAzureConfiguration(options?: StorageOptions): Promise<AzureConfiguration> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.azure;
}
async setAzureConfiguration(value: AzureConfiguration, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.directoryConfigurations.azure = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getOktaConfiguration(options?: StorageOptions): Promise<OktaConfiguration> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.okta;
}
async setOktaConfiguration(value: OktaConfiguration, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.directoryConfigurations.okta = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getOneLoginConfiguration(options?: StorageOptions): Promise<OneLoginConfiguration> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.oneLogin;
}
async setOneLoginConfiguration(
value: OneLoginConfiguration,
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.directoryConfigurations.oneLogin = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getOrganizationId(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.organizationId;
}
async setOrganizationId(value: string, options?: StorageOptions): Promise<void> {
const currentId = await this.getOrganizationId();
if (currentId !== value) {
await this.clearSyncSettings();
}
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.directorySettings.organizationId = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getSync(options?: StorageOptions): Promise<SyncConfiguration> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.sync;
}
async setSync(value: SyncConfiguration, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.directorySettings.sync = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getDirectoryType(options?: StorageOptions): Promise<DirectoryType> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.directoryType;
}
async setDirectoryType(value: DirectoryType, options?: StorageOptions): Promise<void> {
const currentType = await this.getDirectoryType();
if (value !== currentType) {
await this.clearSyncSettings();
}
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.directorySettings.directoryType = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getLastUserSync(options?: StorageOptions): Promise<Date> {
const userSyncDate = (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.lastUserSync;
return userSyncDate ? new Date(userSyncDate) : null;
}
async setLastUserSync(value: Date, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.directorySettings.lastUserSync = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getLastGroupSync(options?: StorageOptions): Promise<Date> {
const groupSyncDate = (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.lastGroupSync;
return groupSyncDate ? new Date(groupSyncDate) : null;
}
async setLastGroupSync(value: Date, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.directorySettings.lastGroupSync = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getLastSyncHash(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directorySettings?.lastSyncHash;
}
async setLastSyncHash(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.directorySettings.lastSyncHash = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getSyncingDir(options?: StorageOptions): Promise<boolean> {
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
?.directorySettings?.syncingDir;
}
async setSyncingDir(value: boolean, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, this.defaultInMemoryOptions)
);
account.directorySettings.syncingDir = value;
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
}
async clearSyncSettings(hashToo = false) {
await this.setUserDelta(null);
await this.setGroupDelta(null);
await this.setLastGroupSync(null);
await this.setLastUserSync(null);
if (hashToo) {
await this.setLastSyncHash(null);
}
}
protected async scaffoldNewAccountStorage(account: Account): Promise<void> {
await this.scaffoldNewAccountDiskStorage(account);
}
protected async scaffoldNewAccountDiskStorage(account: Account): Promise<void> {
const storageOptions = this.reconcileOptions(
{ userId: account.profile.userId },
await this.defaultOnDiskLocalOptions()
);
const storedAccount = await this.getAccount(storageOptions);
if (storedAccount != null) {
account.settings = storedAccount.settings;
account.directorySettings = storedAccount.directorySettings;
account.directoryConfigurations = storedAccount.directoryConfigurations;
} else if (await this.hasTemporaryStorage()) {
// If migrating to state V2 with an no actively authed account we store temporary data to be copied on auth - this will only be run once.
account.settings = await this.storageService.get<any>(keys.tempAccountSettings);
account.directorySettings = await this.storageService.get<any>(keys.tempDirectorySettings);
account.directoryConfigurations = await this.storageService.get<any>(
keys.tempDirectoryConfigs
);
await this.storageService.remove(keys.tempAccountSettings);
await this.storageService.remove(keys.tempDirectorySettings);
await this.storageService.remove(keys.tempDirectoryConfigs);
}
await this.saveAccount(account, storageOptions);
}
protected async pushAccounts(): Promise<void> {
if (this.state?.accounts == null || Object.keys(this.state.accounts).length < 1) {
this.accounts.next(null);
return;
}
this.accounts.next(this.state.accounts);
}
protected async hasTemporaryStorage(): Promise<boolean> {
return (
(await this.storageService.has(keys.tempAccountSettings)) ||
(await this.storageService.has(keys.tempDirectorySettings)) ||
(await this.storageService.has(keys.tempDirectoryConfigs))
);
}
protected resetAccount(account: Account) {
const persistentAccountInformation = {
settings: account.settings,
directorySettings: account.directorySettings,
directoryConfigurations: account.directoryConfigurations,
};
return Object.assign(this.createAccount(), persistentAccountInformation);
}
}

View File

@@ -0,0 +1,158 @@
import { StateVersion } from "jslib-common/enums/stateVersion";
import { StateMigrationService as BaseStateMigrationService } from "jslib-common/services/stateMigration.service";
import { DirectoryType } from "src/enums/directoryType";
import { Account, DirectoryConfigurations, DirectorySettings } from "src/models/account";
import { AzureConfiguration } from "src/models/azureConfiguration";
import { GSuiteConfiguration } from "src/models/gsuiteConfiguration";
import { LdapConfiguration } from "src/models/ldapConfiguration";
import { OktaConfiguration } from "src/models/oktaConfiguration";
import { OneLoginConfiguration } from "src/models/oneLoginConfiguration";
import { SyncConfiguration } from "src/models/syncConfiguration";
const SecureStorageKeys: { [key: string]: any } = {
ldap: "ldapPassword",
gsuite: "gsuitePrivateKey",
azure: "azureKey",
okta: "oktaToken",
oneLogin: "oneLoginClientSecret",
directoryConfigPrefix: "directoryConfig_",
sync: "syncConfig",
directoryType: "directoryType",
userDelta: "userDeltaToken",
groupDelta: "groupDeltaToken",
organizationId: "organizationId",
};
const Keys: { [key: string]: any } = {
entityId: "entityId",
directoryType: "directoryType",
organizationId: "organizationId",
lastUserSync: "lastUserSync",
lastGroupSync: "lastGroupSync",
lastSyncHash: "lastSyncHash",
syncingDir: "syncingDir",
syncConfig: "syncConfig",
tempDirectoryConfigs: "tempDirectoryConfigs",
tempDirectorySettings: "tempDirectorySettings",
};
const ClientKeys: { [key: string]: any } = {
clientIdOld: "clientId",
clientId: "apikey_clientId",
clientSecretOld: "clientSecret",
clientSecret: "apikey_clientSecret",
};
export class StateMigrationService extends BaseStateMigrationService {
async migrate(): Promise<void> {
let currentStateVersion = await this.getCurrentStateVersion();
while (currentStateVersion < StateVersion.Latest) {
switch (currentStateVersion) {
case StateVersion.One:
await this.migrateClientKeys();
await this.migrateStateFrom1To2();
break;
}
currentStateVersion += 1;
}
}
// TODO: remove this migration when we are confident existing api keys are all migrated. Probably 1-2 releases.
protected async migrateClientKeys() {
const oldClientId = await this.storageService.get<string>(ClientKeys.clientIdOld);
const oldClientSecret = await this.storageService.get<string>(ClientKeys.clientSecretOld);
if (oldClientId != null) {
await this.storageService.save(ClientKeys.clientId, oldClientId);
await this.storageService.remove(ClientKeys.clientIdOld);
}
if (oldClientSecret != null) {
await this.storageService.save(ClientKeys.clientSecret, oldClientSecret);
await this.storageService.remove(ClientKeys.clientSecretOld);
}
}
protected async migrateStateFrom1To2(useSecureStorageForSecrets = true): Promise<void> {
// Grabbing a couple of key settings before they get cleared by the base migration
const userId = await this.get<string>(Keys.entityId);
const clientId = await this.get<string>(ClientKeys.clientId);
const clientSecret = await this.get<string>(ClientKeys.clientSecret);
await super.migrateStateFrom1To2();
// Setup reusable method for clearing keys since we will want to do that regardless of if there is an active authenticated session
const clearDirectoryConnectorV1Keys = async () => {
for (const key in Keys) {
if (key == null) {
continue;
}
for (const directoryType in DirectoryType) {
if (directoryType == null) {
continue;
}
await this.set(SecureStorageKeys.directoryConfigPrefix + directoryType, null);
}
}
};
// Initilize typed objects from key/value pairs in storage to either be saved temporarily until an account is authed or applied to the active account
const getDirectoryConfig = async <T>(type: DirectoryType) =>
await this.get<T>(SecureStorageKeys.directoryConfigPrefix + type);
const directoryConfigs: DirectoryConfigurations = {
ldap: await getDirectoryConfig<LdapConfiguration>(DirectoryType.Ldap),
gsuite: await getDirectoryConfig<GSuiteConfiguration>(DirectoryType.GSuite),
azure: await getDirectoryConfig<AzureConfiguration>(DirectoryType.AzureActiveDirectory),
okta: await getDirectoryConfig<OktaConfiguration>(DirectoryType.Okta),
oneLogin: await getDirectoryConfig<OneLoginConfiguration>(DirectoryType.OneLogin),
};
const directorySettings: DirectorySettings = {
directoryType: await this.get<DirectoryType>(Keys.directoryType),
organizationId: await this.get<string>(Keys.organizationId),
lastUserSync: await this.get<Date>(Keys.lastUserSync),
lastGroupSync: await this.get<Date>(Keys.lastGroupSync),
lastSyncHash: await this.get<string>(Keys.lastSyncHash),
syncingDir: await this.get<boolean>(Keys.syncingDir),
sync: await this.get<SyncConfiguration>(Keys.syncConfig),
};
// (userId == null) = no authed account, stored data temporarily to be applied and cleared on next auth
// (userId != null) = authed account known, applied stored data to it and do not save temp data
if (userId == null) {
await this.set(Keys.tempDirectoryConfigs, directoryConfigs);
await this.set(Keys.tempDirectorySettings, directorySettings);
await clearDirectoryConnectorV1Keys();
return;
}
const account = await this.get<Account>(userId);
account.directoryConfigurations = directoryConfigs;
account.directorySettings = directorySettings;
account.profile = {
userId: userId,
entityId: userId,
apiKeyClientId: clientId,
};
account.clientKeys = {
clientId: clientId,
clientSecret: clientSecret,
};
await this.set(userId, account);
await clearDirectoryConnectorV1Keys();
if (useSecureStorageForSecrets) {
for (const key in SecureStorageKeys) {
if (await this.secureStorageService.has(SecureStorageKeys[key])) {
await this.secureStorageService.save(
`${userId}_${SecureStorageKeys[key]}`,
await this.secureStorageService.get(SecureStorageKeys[key])
);
await this.secureStorageService.remove(SecureStorageKeys[key]);
}
}
}
}
}

View File

@@ -1,208 +1,253 @@
import { DirectoryType } from '../enums/directoryType';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { Utils } from "jslib-common/misc/utils";
import { OrganizationImportRequest } from "jslib-common/models/request/organizationImportRequest";
import { GroupEntry } from '../models/groupEntry';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType";
import { GroupEntry } from "../models/groupEntry";
import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry";
import { OrganizationImportRequest } from 'jslib-common/models/request/organizationImportRequest';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { Utils } from 'jslib-common/misc/utils';
import { AzureDirectoryService } from './azure-directory.service';
import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { GSuiteDirectoryService } from './gsuite-directory.service';
import { LdapDirectoryService } from './ldap-directory.service';
import { OktaDirectoryService } from './okta-directory.service';
import { OneLoginDirectoryService } from './onelogin-directory.service';
import { AzureDirectoryService } from "./azure-directory.service";
import { IDirectoryService } from "./directory.service";
import { GSuiteDirectoryService } from "./gsuite-directory.service";
import { LdapDirectoryService } from "./ldap-directory.service";
import { OktaDirectoryService } from "./okta-directory.service";
import { OneLoginDirectoryService } from "./onelogin-directory.service";
export class SyncService {
private dirType: DirectoryType;
private dirType: DirectoryType;
constructor(private configurationService: ConfigurationService, private logService: LogService,
private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService,
private messagingService: MessagingService, private i18nService: I18nService,
private environmentService: EnvironmentService) { }
constructor(
private logService: LogService,
private cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService,
private messagingService: MessagingService,
private i18nService: I18nService,
private environmentService: EnvironmentService,
private stateService: StateService
) {}
async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
this.dirType = await this.configurationService.getDirectoryType();
if (this.dirType == null) {
throw new Error('No directory configured.');
}
const directoryService = this.getDirectoryService();
if (directoryService == null) {
throw new Error('Cannot load directory service.');
}
const syncConfig = await this.configurationService.getSync();
const startingGroupDelta = await this.configurationService.getGroupDeltaToken();
const startingUserDelta = await this.configurationService.getUserDeltaToken();
const now = new Date();
this.messagingService.send('dirSyncStarted');
try {
const entries = await directoryService.getEntries(force || syncConfig.overwriteExisting, test);
let groups = entries[0];
let users = this.filterUnsupportedUsers(entries[1]);
if (groups != null && groups.length > 0) {
this.flattenUsersToGroups(groups, groups);
}
users = this.removeDuplicateUsers(users);
if (test || (!syncConfig.overwriteExisting &&
(groups == null || groups.length === 0) && (users == null || users.length === 0))) {
if (!test) {
await this.saveSyncTimes(syncConfig, now);
}
this.messagingService.send('dirSyncCompleted', { successfully: true });
return [groups, users];
}
const req = this.buildRequest(groups, users, syncConfig.removeDisabled, syncConfig.overwriteExisting, syncConfig.largeImport);
const reqJson = JSON.stringify(req);
const orgId = await this.configurationService.getOrganizationId();
if (orgId == null) {
throw new Error('Organization not set.');
}
// TODO: Remove hashLegacy once we're sure clients have had time to sync new hashes
let hashLegacy: string = null;
const hashBuffLegacy = await this.cryptoFunctionService.hash(this.environmentService.getApiUrl() + reqJson, 'sha256');
if (hashBuffLegacy != null) {
hashLegacy = Utils.fromBufferToB64(hashBuffLegacy);
}
let hash: string = null;
const hashBuff = await this.cryptoFunctionService.hash(this.environmentService.getApiUrl() + orgId + reqJson, 'sha256');
if (hashBuff != null) {
hash = Utils.fromBufferToB64(hashBuff);
}
const lastHash = await this.configurationService.getLastSyncHash();
if (lastHash == null || (hash !== lastHash && hashLegacy !== lastHash)) {
await this.apiService.postPublicImportDirectory(req);
await this.configurationService.saveLastSyncHash(hash);
} else {
groups = null;
users = null;
}
await this.saveSyncTimes(syncConfig, now);
this.messagingService.send('dirSyncCompleted', { successfully: true });
return [groups, users];
} catch (e) {
if (!test) {
await this.configurationService.saveGroupDeltaToken(startingGroupDelta);
await this.configurationService.saveUserDeltaToken(startingUserDelta);
}
this.messagingService.send('dirSyncCompleted', { successfully: false });
throw e;
}
async sync(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
this.dirType = await this.stateService.getDirectoryType();
if (this.dirType == null) {
throw new Error("No directory configured.");
}
private removeDuplicateUsers(users: UserEntry[]) {
const uniqueUsers = new Array<UserEntry>();
const processedUsers = new Map<string, string>();
const duplicateEmails = new Array<string>();
// UserEntrys with the same email are ignored if their properties are the same
// UserEntrys with the same email but different properties will throw an error
users.forEach(u => {
if (processedUsers.has(u.email)) {
if (processedUsers.get(u.email) != JSON.stringify(u)) {
duplicateEmails.push(u.email);
}
} else {
uniqueUsers.push(u);
processedUsers.set(u.email, JSON.stringify(u));
}
});
if (duplicateEmails.length > 0) {
const emailsMessage = duplicateEmails.length < 4 ?
duplicateEmails.join('\n') :
duplicateEmails.slice(0, 3).join('\n') + '\n' + this.i18nService.t('andMore', `${duplicateEmails.length - 3}`);
throw new Error(this.i18nService.t('duplicateEmails') + '\n' + emailsMessage);
}
return uniqueUsers;
const directoryService = this.getDirectoryService();
if (directoryService == null) {
throw new Error("Cannot load directory service.");
}
private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] {
return users == null ? null : users.filter(u => u.email?.length <= 256);
const syncConfig = await this.stateService.getSync();
const startingGroupDelta = await this.stateService.getGroupDelta();
const startingUserDelta = await this.stateService.getUserDelta();
const now = new Date();
this.messagingService.send("dirSyncStarted");
try {
const entries = await directoryService.getEntries(
force || syncConfig.overwriteExisting,
test
);
let groups = entries[0];
let users = this.filterUnsupportedUsers(entries[1]);
if (groups != null && groups.length > 0) {
this.flattenUsersToGroups(groups, groups);
}
users = this.removeDuplicateUsers(users);
if (
test ||
(!syncConfig.overwriteExisting &&
(groups == null || groups.length === 0) &&
(users == null || users.length === 0))
) {
if (!test) {
await this.saveSyncTimes(syncConfig, now);
}
this.messagingService.send("dirSyncCompleted", { successfully: true });
return [groups, users];
}
const req = this.buildRequest(
groups,
users,
syncConfig.removeDisabled,
syncConfig.overwriteExisting,
syncConfig.largeImport
);
const reqJson = JSON.stringify(req);
const orgId = await this.stateService.getOrganizationId();
if (orgId == null) {
throw new Error("Organization not set.");
}
// TODO: Remove hashLegacy once we're sure clients have had time to sync new hashes
let hashLegacy: string = null;
const hashBuffLegacy = await this.cryptoFunctionService.hash(
this.environmentService.getApiUrl() + reqJson,
"sha256"
);
if (hashBuffLegacy != null) {
hashLegacy = Utils.fromBufferToB64(hashBuffLegacy);
}
let hash: string = null;
const hashBuff = await this.cryptoFunctionService.hash(
this.environmentService.getApiUrl() + orgId + reqJson,
"sha256"
);
if (hashBuff != null) {
hash = Utils.fromBufferToB64(hashBuff);
}
const lastHash = await this.stateService.getLastSyncHash();
if (lastHash == null || (hash !== lastHash && hashLegacy !== lastHash)) {
await this.apiService.postPublicImportDirectory(req);
await this.stateService.setLastSyncHash(hash);
} else {
groups = null;
users = null;
}
await this.saveSyncTimes(syncConfig, now);
this.messagingService.send("dirSyncCompleted", { successfully: true });
return [groups, users];
} catch (e) {
if (!test) {
await this.stateService.setGroupDelta(startingGroupDelta);
await this.stateService.setUserDelta(startingUserDelta);
}
this.messagingService.send("dirSyncCompleted", { successfully: false });
throw e;
}
}
private removeDuplicateUsers(users: UserEntry[]) {
if (users == null) {
return null;
}
private flattenUsersToGroups(levelGroups: GroupEntry[], allGroups: GroupEntry[]): Set<string> {
let allUsers = new Set<string>();
if (allGroups == null) {
return allUsers;
const uniqueUsers = new Array<UserEntry>();
const processedActiveUsers = new Map<string, string>();
const processedDeletedUsers = new Map<string, string>();
const duplicateEmails = new Array<string>();
// UserEntrys with the same email are ignored if their properties are the same
// UserEntrys with the same email but different properties will throw an error, unless they are all in a deleted state.
users.forEach((u) => {
if (processedActiveUsers.has(u.email)) {
if (processedActiveUsers.get(u.email) !== JSON.stringify(u)) {
duplicateEmails.push(u.email);
}
for (const group of levelGroups) {
const childGroups = allGroups.filter(g => group.groupMemberReferenceIds.has(g.referenceId));
const childUsers = this.flattenUsersToGroups(childGroups, allGroups);
childUsers.forEach(id => group.userMemberExternalIds.add(id));
allUsers = new Set([...allUsers, ...group.userMemberExternalIds]);
} else {
if (!u.deleted) {
// Check that active UserEntry does not conflict with a deleted UserEntry
if (processedDeletedUsers.has(u.email)) {
duplicateEmails.push(u.email);
} else {
processedActiveUsers.set(u.email, JSON.stringify(u));
uniqueUsers.push(u);
}
} else {
// UserEntrys with duplicate email will not throw an error if they are all deleted. They will be synced.
processedDeletedUsers.set(u.email, JSON.stringify(u));
uniqueUsers.push(u);
}
return allUsers;
}
});
if (duplicateEmails.length > 0) {
const emailsMessage =
duplicateEmails.length < 4
? duplicateEmails.join("\n")
: duplicateEmails.slice(0, 3).join("\n") +
"\n" +
this.i18nService.t("andMore", `${duplicateEmails.length - 3}`);
throw new Error(this.i18nService.t("duplicateEmails") + "\n" + emailsMessage);
}
private getDirectoryService(): IDirectoryService {
switch (this.dirType) {
case DirectoryType.GSuite:
return new GSuiteDirectoryService(this.configurationService, this.logService, this.i18nService);
case DirectoryType.AzureActiveDirectory:
return new AzureDirectoryService(this.configurationService, this.logService, this.i18nService);
case DirectoryType.Ldap:
return new LdapDirectoryService(this.configurationService, this.logService, this.i18nService);
case DirectoryType.Okta:
return new OktaDirectoryService(this.configurationService, this.logService, this.i18nService);
case DirectoryType.OneLogin:
return new OneLoginDirectoryService(this.configurationService, this.logService, this.i18nService);
default:
return null;
}
}
return uniqueUsers;
}
private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean, overwriteExisting: boolean,
largeImport: boolean = false) {
return new OrganizationImportRequest({
groups: (groups ?? []).map(g => {
return {
name: g.name,
externalId: g.externalId,
memberExternalIds: Array.from(g.userMemberExternalIds),
};
}),
users: (users ?? []).map(u => {
return {
email: u.email,
externalId: u.externalId,
deleted: u.deleted || (removeDisabled && u.disabled),
};
}),
overwriteExisting: overwriteExisting,
largeImport: largeImport,
});
}
private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] {
return users == null ? null : users.filter((u) => u.email?.length <= 256);
}
private async saveSyncTimes(syncConfig: SyncConfiguration, time: Date) {
if (syncConfig.groups) {
await this.configurationService.saveLastGroupSyncDate(time);
}
if (syncConfig.users) {
await this.configurationService.saveLastUserSyncDate(time);
}
private flattenUsersToGroups(levelGroups: GroupEntry[], allGroups: GroupEntry[]): Set<string> {
let allUsers = new Set<string>();
if (allGroups == null) {
return allUsers;
}
for (const group of levelGroups) {
const childGroups = allGroups.filter((g) => group.groupMemberReferenceIds.has(g.referenceId));
const childUsers = this.flattenUsersToGroups(childGroups, allGroups);
childUsers.forEach((id) => group.userMemberExternalIds.add(id));
allUsers = new Set([...allUsers, ...group.userMemberExternalIds]);
}
return allUsers;
}
private getDirectoryService(): IDirectoryService {
switch (this.dirType) {
case DirectoryType.GSuite:
return new GSuiteDirectoryService(this.logService, this.i18nService, this.stateService);
case DirectoryType.AzureActiveDirectory:
return new AzureDirectoryService(this.logService, this.i18nService, this.stateService);
case DirectoryType.Ldap:
return new LdapDirectoryService(this.logService, this.i18nService, this.stateService);
case DirectoryType.Okta:
return new OktaDirectoryService(this.logService, this.i18nService, this.stateService);
case DirectoryType.OneLogin:
return new OneLoginDirectoryService(this.logService, this.i18nService, this.stateService);
default:
return null;
}
}
private buildRequest(
groups: GroupEntry[],
users: UserEntry[],
removeDisabled: boolean,
overwriteExisting: boolean,
largeImport = false
) {
return new OrganizationImportRequest({
groups: (groups ?? []).map((g) => {
return {
name: g.name,
externalId: g.externalId,
memberExternalIds: Array.from(g.userMemberExternalIds),
};
}),
users: (users ?? []).map((u) => {
return {
email: u.email,
externalId: u.externalId,
deleted: u.deleted || (removeDisabled && u.disabled),
};
}),
overwriteExisting: overwriteExisting,
largeImport: largeImport,
});
}
private async saveSyncTimes(syncConfig: SyncConfiguration, time: Date) {
if (syncConfig.groups) {
await this.stateService.setLastGroupSync(time);
}
if (syncConfig.users) {
await this.stateService.setLastUserSync(time);
}
}
}

View File

@@ -1,100 +1,105 @@
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { SyncService } from './services/sync.service';
import { Entry } from './models/entry';
import { LdapConfiguration } from './models/ldapConfiguration';
import { SimResult } from './models/simResult';
import { SyncConfiguration } from './models/syncConfiguration';
import { UserEntry } from './models/userEntry';
import { Entry } from "./models/entry";
import { LdapConfiguration } from "./models/ldapConfiguration";
import { SimResult } from "./models/simResult";
import { SyncConfiguration } from "./models/syncConfiguration";
import { UserEntry } from "./models/userEntry";
import { SyncService } from "./services/sync.service";
export class ConnectorUtils {
static async simulate(syncService: SyncService, i18nService: I18nService, sinceLast: boolean): Promise<SimResult> {
return new Promise(async (resolve, reject) => {
const simResult = new SimResult();
try {
const result = await syncService.sync(!sinceLast, true);
if (result[0] != null) {
simResult.groups = result[0];
}
if (result[1] != null) {
simResult.users = result[1];
}
} catch (e) {
simResult.groups = null;
simResult.users = null;
reject(e || i18nService.t('syncError'));
return;
}
static async simulate(
syncService: SyncService,
i18nService: I18nService,
sinceLast: boolean
): Promise<SimResult> {
// eslint-disable-next-line
return new Promise(async (resolve, reject) => {
const simResult = new SimResult();
try {
const result = await syncService.sync(!sinceLast, true);
if (result[0] != null) {
simResult.groups = result[0];
}
if (result[1] != null) {
simResult.users = result[1];
}
} catch (e) {
simResult.groups = null;
simResult.users = null;
reject(e || i18nService.t("syncError"));
return;
}
const userMap = new Map<string, UserEntry>();
this.sortEntries(simResult.users, i18nService);
for (const u of simResult.users) {
userMap.set(u.externalId, u);
if (u.deleted) {
simResult.deletedUsers.push(u);
} else if (u.disabled) {
simResult.disabledUsers.push(u);
} else {
simResult.enabledUsers.push(u);
}
}
const userMap = new Map<string, UserEntry>();
this.sortEntries(simResult.users, i18nService);
for (const u of simResult.users) {
userMap.set(u.externalId, u);
if (u.deleted) {
simResult.deletedUsers.push(u);
} else if (u.disabled) {
simResult.disabledUsers.push(u);
} else {
simResult.enabledUsers.push(u);
}
}
this.sortEntries(simResult.groups, i18nService);
for (const g of simResult.groups) {
if (g.userMemberExternalIds == null) {
continue;
}
const anyG = (g as any);
anyG.users = [];
for (const uid of g.userMemberExternalIds) {
if (userMap.has(uid)) {
anyG.users.push(userMap.get(uid));
} else {
anyG.users.push({ displayName: uid });
}
}
this.sortEntries(anyG.users, i18nService);
}
resolve(simResult);
});
}
static adjustConfigForSave(ldap: LdapConfiguration, sync: SyncConfiguration) {
if (ldap.ad) {
sync.creationDateAttribute = 'whenCreated';
sync.revisionDateAttribute = 'whenChanged';
sync.emailPrefixAttribute = 'sAMAccountName';
sync.memberAttribute = 'member';
sync.userObjectClass = 'person';
sync.groupObjectClass = 'group';
sync.userEmailAttribute = 'mail';
sync.groupNameAttribute = 'name';
if (sync.groupPath == null) {
sync.groupPath = 'CN=Users';
}
if (sync.userPath == null) {
sync.userPath = 'CN=Users';
}
this.sortEntries(simResult.groups, i18nService);
for (const g of simResult.groups) {
if (g.userMemberExternalIds == null) {
continue;
}
if (sync.interval != null) {
if (sync.interval <= 0) {
sync.interval = null;
} else if (sync.interval < 5) {
sync.interval = 5;
}
const anyG = g as any;
anyG.users = [];
for (const uid of g.userMemberExternalIds) {
if (userMap.has(uid)) {
anyG.users.push(userMap.get(uid));
} else {
anyG.users.push({ displayName: uid });
}
}
this.sortEntries(anyG.users, i18nService);
}
resolve(simResult);
});
}
static adjustConfigForSave(ldap: LdapConfiguration, sync: SyncConfiguration) {
if (ldap.ad) {
sync.creationDateAttribute = "whenCreated";
sync.revisionDateAttribute = "whenChanged";
sync.emailPrefixAttribute = "sAMAccountName";
sync.memberAttribute = "member";
sync.userObjectClass = "person";
sync.groupObjectClass = "group";
sync.userEmailAttribute = "mail";
sync.groupNameAttribute = "name";
if (sync.groupPath == null) {
sync.groupPath = "CN=Users";
}
if (sync.userPath == null) {
sync.userPath = "CN=Users";
}
}
private static sortEntries(arr: Entry[], i18nService: I18nService) {
arr.sort((a, b) => {
return i18nService.collator ? i18nService.collator.compare(a.displayName, b.displayName) :
a.displayName.localeCompare(b.displayName);
});
if (sync.interval != null) {
if (sync.interval <= 0) {
sync.interval = null;
} else if (sync.interval < 5) {
sync.interval = 5;
}
}
}
private static sortEntries(arr: Entry[], i18nService: I18nService) {
arr.sort((a, b) => {
return i18nService.collator
? i18nService.collator.compare(a.displayName, b.displayName)
: a.displayName.localeCompare(b.displayName);
});
}
}

Some files were not shown because too many files have changed in this diff Show More