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

Compare commits

...

245 Commits

Author SHA1 Message Date
Thomas Rittson
9cfa646bcb Merge pull request #102 from bitwarden/cert-empty-subject
Fix handling of empty subject names in certs
2021-03-11 13:11:36 +10:00
Thomas Rittson
b4301c7d41 Fix handling of empty subject names in certs 2021-03-11 12:43:29 +10:00
Chad Scharf
71b5f6a38a Merge pull request #101 from bitwarden/version-bump
Patch release version bump to 2.9.1
2021-03-10 12:13:32 -05:00
Thomas Rittson
1c0052fe30 Patch release version bump to 2.9.1 2021-03-10 09:05:02 +10:00
Matt Gibson
35862acb73 Update jslib (#100) 2021-03-09 11:33:45 -06:00
Chad Scharf
11cf64fcc7 Merge pull request #99 from bitwarden/fix/deleted-user-fail
Don't check user group filter for deleted users
2021-03-05 18:04:01 -05:00
Chad Scharf
2ab37b45cf Don't check user group filter for deleted users 2021-03-05 15:49:57 -05:00
Joseph Flinn
7096fc830b adding the build assets for the rc branch (#98) 2021-03-04 10:25:23 -08:00
Matt Gibson
39806b7d96 Update jslib (#97) 2021-03-02 13:30:11 -06:00
Kyle Spearrin
7a16b8cb0e Update README.md 2021-02-17 13:17:25 -05:00
Matt Gibson
2583068dbd Lock lowdb file (#95)
* Lock lowdb file when using. Do not allow caching

* Linter fixes

* Move to non-jslib lowdbstorage to allow for lockfile

* update jslib

* Must ensure db file exists prior to initialization

proper-lockfile throws if the file its locking does not exist

* update jslib

* Let base handle file initialization
2021-02-17 10:33:05 -06:00
Matt Gibson
e5d0b3a372 Update to commander 7 (#94)
* Upgrade to commander 7.0.0

* Match lint rules for typescript

* update jslib
2021-02-08 13:32:02 -06:00
Oscar Hinton
9a1caf1e7e Update electron to 11.1.1 (#85) 2021-02-03 23:08:50 +01:00
Kyle Spearrin
af0e41e26c dont catch api error and return false (#93) 2021-02-03 15:58:37 -05:00
Joseph Flinn
d3049164a9 Migrate to gh actions (#89)
* intial go at building the windows pipeline in GH

* fixing whitespace issue

* moving version info script

* changing the electron-builder commands to the npm scripts

* fixing the PACKAGE_VERSION var

* adding debugging statements

* changing list command

* fixing PACKAGE_VERSION var

* adding linux job and disabling windows job

* debugging linux installs

* retrying the rpm

* re-enabling the windows build

* re-enabling publishing of the exe

* debugging pkg fetched

* debugging this more

* testing install of pkg-fetch with npm

* moving pkg-fetch installation

* trying to manually add the fetched package

* I was wrong. This wasn't linux. Switching to pwsh

* fixing the pwsh var syntax

* removing debugging tasks and re-enabling the other build tasks

* adding build_and_signing. Removing the non-cli executables from the build pipeline and disabling it for testing.

* removing some whitespace

* switching how we get package version

* adding custom signing script

* removing deubbing code and getting ready for PR

* adding in another release gate

* chaning file name to fit previous standards

* removing appveyor pipeline file

* moving all of the build tasks to the same build file

* changing GITHUB_TOKEN because GITHUB_* is probably reserved

* adding release pipeline and moving all realease tasks to that pipeline

* updating the package.json's to contain the releases to my repo

* fixing the RELEASE_TAG_NAME and switching the electron builder from pack to publish

* fixing the npm run publish command

* adding GH_TOKEN to the build and sign task

* fixing upload path

* removing the release asset upload since I think they are already published?

* removing testing code

* testing tweak to github release

* making sure I've got the right repo set

* removing whitespace

* adding in clone task to setup

* removing the stop-gap

* adding GH_TOKEN to the linux publish task

* fixing string

* switching to manual publishing. There seems to be a bug in the electron-builder publishing? or our setup

* switching back to electron-builder publishing but manually creating and pushing the tag

* I don't know why electron-builder isn't picking up the release. Adding some debugging code

* adding in GH token for release checking

* adding another GH token for release checking

* commenting out the tagging portion. This should just happen automatically...

* trying the release without the manual uploads?

* adding -d flag to release edit

* disabling the gui build to see if the cli changes the tag

* trying out a fix

* testing the upload release asset action

* fixing typo

* trying RELEASE_NAME

* fixing bash error

* trying something else for the release name

* changing all of the release asset uploads to a provided action

* Removing some debugging code

* re-enabling the windows and linux jobs

* changing the content type of the checksum files

* fixing typo

* removing the PKG_INFO flag

* installing RH with choco

* testing the reshack

* reenabling the correct job

* resetting release workflow and adding exp workflow

* trying ResourceHacker.exe

* switching to pwsh to see if that works

* switching back and specifying cmd shell

* finding the bin to add to the path

* wrestling with cmd

* debugging path

* giving up on nice printing

* changing to different path debugging

* adding RH to the path

* trying something else

* trying something else

* maybe the path resets?

* updating exp workflow to try to get reshack to work

* trying to add to the path without the quotes

* fixing the RH test

* debugging path

* setting path forever

* not playing around with perfect environment paths with windows....

* preivous test was inconclusive

* testing RH

* changing the npm command and removing unnecssary GITHUB_TOKEN

* removing the exp workflow

* quoting the signing file

* debugging VER_INFO

* debugging the pkg-fetch

* disabling non-cli jobs

* changing value of WIN_PKG

* testing more pkg-fetch

* changing the paths to the home directory

* renaming exp workflow

* trying a string

* trying it from the home directory

* removing the stop gap

* updating the version to something that RH supports

* initial release test

* fixing GITHUB_TOKEN

* changing the version to a real version

* debugging tag names

* changing the trigger on the exp workflow

* moving the disabled job to the correct workflow

* trying wet spaghetti

* updating case statement

* adding in the findings from the experiment

* removing testing code. Leaving unfinished macos build disabled

* removing the prod environment secrets

* setting up the mac build job

* renaming the key name

* moving the signing file

* working on the mac packaging

* removing desktop mac certs

* disabling the non-mac jobs

* setting up the build workflow for first run

* adding manual trigger to the build workflow

* disabling the push trigger

* removing the non-existant setup function

* removing the unneeded certs

* removing increment version since we are not submitting to the Apple Store.

* re-enabling the APPLE_ID vars

* updating how the package version is retrieved in build. staging release workflow for testing

* fixing the asset upload updating the repo in package.json

* adding debugging to dist

* adding in missing directory for debugging

* renaming that file

* updating the build/release workflows

* fixing the setup output

* updating file name and changing dist to publish

* adding in the missing token

* changing the zip name

* add debuggin

* fixing debugging step

* removing debugging task. Not needed

* reworking the content type of the mac release assets

* removing the rename task and adding in some debugging

* flipping the order of the dmg and the mac.zip upload to see if it is a problem with the release asset upload

* adding the renaming back in

* switching the upload name back to dashes

* commenting out the manual release asset upload. Looks like publish is doing that?

* removing all debugging code

* updating README with the GitHub Actions Badge

* changing all of the slashes to match

* removing unneeded package version setting

* removing unneeded package version setup

* adding WIN_PKG task back in. accidentally removed it
2021-02-03 09:12:43 -08:00
Thomas Rittson
bdfda6775d Merge pull request #90 from bitwarden/bump-https-proxy-agent
Bump https proxy agent
2021-02-02 12:59:25 +10:00
Thomas Rittson
72e6e74a42 add allowSyntheticDefaultImports in tsconfig 2021-02-02 10:32:18 +10:00
Thomas Rittson
75832fbed9 bump https-proxy-agent verison to 5.0 2021-02-01 16:50:47 +10:00
Thomas Rittson
811b3adb51 Update jslib 2021-02-01 16:31:16 +10:00
Kyle Spearrin
e710df3ba7 certs for CI 2021-01-29 12:29:30 -05:00
Chad Scharf
4ee0c3ccba Merge pull request #87 from bitwarden/version-bump-2.9.0
version bump 2.9.0
2021-01-19 16:18:08 -05:00
Chad Scharf
036b934119 version bump 2.9.0 2021-01-19 15:15:15 -05:00
Vincent Salucci
4bfd43bf4c [jslib] Update (48144a7) (#81)
* update jslib (48144a7)

* Fixed breaking changes for previous jslib updates
2021-01-05 17:34:18 -06:00
Pasi Niemi
55722d3c04 Add options for giving passwords and secrets as file contents or in an environment variable (#82) 2021-01-04 11:53:11 -05:00
Chad Scharf
77043d8d66 Merge pull request #44 from NitorCreations/filter_by_administrativeunit
Enable filtering Azure AD directory by administrative unit
2020-12-30 18:10:10 -05:00
Pasi Niemi
002117a6e5 Fix as per the comment about naming and visibility conventions 2020-12-30 22:53:49 +02:00
Vincent Salucci
5aa8097cfd Update jslib (abb54f0 -> 72bf18f) (#79) 2020-12-09 09:48:30 -06:00
Kyle Spearrin
19e1049566 use env variable for apple id password 2020-12-03 23:10:30 -05:00
Kyle Spearrin
87bdc88e22 bump version 2020-12-03 23:01:06 -05:00
Chad Scharf
950e3ae91e Merge pull request #78 from bitwarden/update-jslib-sso
jslib update to fix SSO from DC
2020-12-02 14:52:53 -05:00
Chad Scharf
a37532e1ad Merge branch 'master' into update-jslib-sso
merge lastest from master into jslib update branch
2020-12-02 14:27:55 -05:00
Chad Scharf
21ff5f311b Merge pull request #77 from bitwarden/update-jslib
Update jslib
2020-12-02 14:24:30 -05:00
Chad Scharf
7dd12cf0cb jslib update to fix SSO from DC 2020-12-02 14:16:30 -05:00
Chad Scharf
6f8df7a690 Update jslib 2020-11-23 16:08:12 -05:00
Chad Scharf
1ff0ef1f90 Merge pull request #75 from bitwarden/fix-build-error
Revert https-proxy-agent version
2020-11-13 15:07:31 -05:00
Chad Scharf
196fc10d80 Revert https-proxy-agent version 2020-11-13 14:54:36 -05:00
Chad Scharf
7496de4cc6 Merge pull request #74 from kitos9112/master
Bump https-proxy-agent dep. to 5.0.0
2020-11-13 14:09:02 -05:00
Marcos Soutullo Rodriguez
02a6adf6a2 Bump https-proxy-agent dep. to 5.0.0 2020-11-13 18:51:54 +00:00
Chad Scharf
6c95575a8f Merge pull request #73 from bitwarden/version-jslib-update
Version bump + jslib update
2020-10-21 09:46:19 -04:00
Chad Scharf
39755e89a8 Version bump + jslib update 2020-10-21 09:39:54 -04:00
Chad Scharf
c5d3ca218e Merge pull request #71 from bitwarden/fix/bwdc-lint-errors
Fix lint errors/warnings
2020-10-20 10:22:49 -04:00
Chad Scharf
87a2a2a0e4 Merge pull request #72 from bitwarden/add-logging-to-lowdb-storage-svc
Add logger to lowdb storage service
2020-10-20 09:33:38 -04:00
Chad Scharf
5904da2eb1 Add logger to lowdb storage service 2020-10-19 16:05:15 -04:00
Chad Scharf
1ac0c81661 Fix lint errors/warnings 2020-10-19 13:50:08 -04:00
Vincent Salucci
955711714d [SSO] New user provision flow jslib update (5e0a2d1 -> d84d6da) (#70)
* Updated import/constructor

* Update jslib (5e0a2d1 -> d84d6da)
2020-10-14 08:59:40 -05:00
Pasi Niemi
5848553a4b Merge remote-tracking branch 'upstream/master' into filter_by_administrativeunit 2020-09-24 12:36:32 +03:00
Kyle Spearrin
38758caac4 update jslib 2020-09-23 12:02:31 -04:00
Kyle Spearrin
b41a1bdbf4 bump version 2020-09-15 16:38:50 -04:00
Chad Scharf
a400ab7f7d update jslib (#67) 2020-09-15 11:00:20 -04:00
Vincent Salucci
e5d0405882 Added await to prevent race condition for successRoute (#66) 2020-09-10 12:35:52 -05:00
Vincent Salucci
f2137c02f7 Adjusted new success route (#65) 2020-09-09 09:22:54 -05:00
Kyle Spearrin
4c61f465a3 update jslib 2020-09-08 11:24:49 -04:00
Kyle Spearrin
6d22041eab update jslib 2020-09-03 10:35:55 -04:00
Vincent Salucci
752e26db6d [SSO] Adjust set password flow (#64)
* Removed set-password // Adjusted sso UI // Added string

* Removed test logic

* Update jslib (6ab444a -> 700e945)

* Passing null for expectes sync service

* Removing unnecessary async

* Deleted set-password files. RIP

* Removed non-existent import (fixes build)
2020-08-28 09:54:45 -05:00
Vincent Salucci
094ec23f04 Merge pull request #62 from bitwarden/m-set-password
[SSO] Added set password flow
2020-08-24 13:10:30 -05:00
Vincent Salucci
af8ff2901e Fixed theme colors // fixed formatting issues in misc 2020-08-24 11:50:25 -05:00
Vincent Salucci
628689c990 Removed notorization shortcut // Cleaned up extra comma in the service module 2020-08-22 14:43:40 -05:00
Vincent Salucci
ab37221182 Updated UI for password strength // Updated success route // Add more missing strings 2020-08-22 14:28:22 -05:00
Vincent Salucci
626892473f Updated dependency chain // Updated UI // Added Policy Service // Added missing strings // Added callout styles 2020-08-22 12:47:50 -05:00
Vincent Salucci
c621677852 update jslib (5d874d0 -> 6ab444a) 2020-08-22 08:06:08 -05:00
Vincent Salucci
6650b4848d Initial commit for bootstrap styled set-password 2020-08-21 09:24:09 -05:00
Vincent Salucci
c07c56f89b Initial commit of set password flow 2020-08-20 21:32:20 -05:00
Oscar Hinton
294590882f Upgrade to Angular 9 (#60) 2020-08-19 15:49:56 -04:00
Kyle Spearrin
9151b9c2d6 support launching sso from login page button (#59) 2020-08-12 16:03:41 -04:00
Kyle Spearrin
5817468d09 fix login ctor 2020-08-11 14:11:47 -04:00
Kyle Spearrin
31dd20999c update jslib 2020-08-11 12:07:55 -04:00
A Codeweavers Infrastructure Bod
4eb9c9bd4d Fix for #55 (#56)
* Groups that reference all users in an organisation were not being populated

They now are, based on the "customer" member type. The null check for member.status is now not required as the property was only null for groups and now that comparison will not occur.

* If the user is not configured to sync users, but is syncing groups this errored with "users is not iterable"

* Update gsuite-directory.service.ts

Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
2020-08-10 09:39:00 -04:00
Kyle Spearrin
150164534f sso login from bwdc desktop app (#58) 2020-08-05 11:09:06 -04:00
Kyle Spearrin
2b2d8a9fab CLI support for SSO Login (#57)
* sso login support

* fix build and lint issues

* allow web vault URL to be set
2020-08-04 14:19:53 -04:00
Kyle Spearrin
fb122cbbdb fix okta paging (#51)
* fix okta paging

* remove okta package

* use node https instead of a library

* remove bent types

* add 500ms throttle to avoid rate limiter
2020-07-02 14:50:54 -04:00
Kyle Spearrin
0b37857d29 formatting fix 2020-07-02 13:32:08 -04:00
Kyle Spearrin
15c1876687 dont skip disbaled users if not removeDisabled (#50) 2020-07-01 16:15:32 -04:00
Kyle Spearrin
473a6e391d update jslib 2020-06-26 15:23:06 -04:00
Kyle Spearrin
13ad64e6f3 if email field is not valid, try username 2020-06-22 10:43:55 -04:00
Kyle Spearrin
04e278249e add support for email coming from username prop (#47)
* add support for email coming from username prop

* feedback adjustments
2020-06-22 10:28:06 -04:00
Kyle Spearrin
e12c4ea1e2 include all members of the group (#46) 2020-06-16 14:42:38 -04:00
Kyle Spearrin
acfa8632af ignore case 2020-06-04 09:59:45 -04:00
Kyle Spearrin
f53c1f5605 update jslib 2020-05-29 11:10:14 -04:00
Kyle Spearrin
f0f7f89ea8 null checks 2020-05-22 14:28:20 -04:00
Kyle Spearrin
059ff0647a Added option to let user control the paged param (#45) 2020-05-19 12:16:30 -04:00
Kyle Spearrin
d94e5b0620 debug paged = false 2020-05-19 10:27:40 -04:00
Pasi Niemi
3840bce6d7 Enable filtering Azure AD directory by administrative unit 2020-05-19 08:00:25 +03:00
Kyle Spearrin
71cd11eedf update webpack to use MiniCssExtractPlugin 2020-05-08 12:18:03 -04:00
Kyle Spearrin
ecea04bc08 brand color updates 2020-05-05 16:59:26 -04:00
Kyle Spearrin
0575ee5507 skip null user emails 2020-05-04 08:35:39 -04:00
Kyle Spearrin
8b2fa8405b add paging to group members api request 2020-04-24 11:25:00 -04:00
Kyle Spearrin
7d36a50687 update lunr types (#37) 2020-04-14 15:55:39 -04:00
Kyle Spearrin
f7dd9d8d5b filter unsupported user entries by email length 2020-03-27 10:36:41 -04:00
Kyle Spearrin
379b4f4612 update jslib 2020-03-21 00:20:41 -04:00
Kyle Spearrin
4652668e1b Enable OneLogin type from settings 2020-03-13 23:16:19 -04:00
Kyle Spearrin
4e02a8571e UserSelectParams for Azure AD 2020-03-13 23:15:39 -04:00
Kyle Spearrin
bc927a65ac if no dc= in rootPath, just return pathPrefix 2020-03-13 10:12:17 -04:00
Kyle Spearrin
90f1a1c115 noiteration option 2020-03-13 09:06:01 -04:00
Kyle Spearrin
c48acf6038 support onelogin in cli 2020-03-13 09:04:40 -04:00
Kyle Spearrin
2640e8c890 onelogin directory implementation 2020-03-12 22:25:38 -04:00
Kyle Spearrin
094ec55e7f update jslib 2020-03-12 20:27:44 -04:00
Kyle Spearrin
fe39bdac42 remove request interceptor 2020-03-12 14:44:09 -04:00
Kyle Spearrin
2c98d50c43 remove old client ref 2020-03-12 12:53:26 -04:00
Kyle Spearrin
d374cff51c stub out OneLogin connector 2020-03-12 12:42:04 -04:00
Kyle Spearrin
d9e7256804 upgrade electron builder 2020-03-06 11:20:22 -05:00
Kyle Spearrin
0ef2d1523e Revert "update electron builder/updater"
This reverts commit 05bad6f671.
2020-03-06 11:19:13 -05:00
Kyle Spearrin
05bad6f671 update electron builder/updater 2020-03-06 09:24:27 -05:00
Kyle Spearrin
5a62cfcda1 4.2 updater 2020-03-05 10:43:31 -05:00
Kyle Spearrin
634d38510d bump version 2020-03-05 09:32:05 -05:00
Kyle Spearrin
bf27872973 update jslib. tweaks to start tls 2020-03-04 11:31:35 -05:00
Colin Campbell
20bb5a4926 starttls: Support LDAP STARTTLS (#33)
* starttls: Support LDAP STARTTLS

* starttls: Re-roll to preserve old config
2020-03-04 11:08:47 -05:00
Kyle Spearrin
f63fb3ffa0 bitwarden inc. 2020-02-18 22:36:38 -05:00
Kyle Spearrin
f11c32c606 update https proxy 2020-01-27 14:00:29 -05:00
Kyle Spearrin
0b4e22a952 symlink jslib on mac/lin 2020-01-27 12:46:18 -05:00
Kyle Spearrin
a85dbff3a5 upgrade to electron 6 2020-01-27 10:00:21 -05:00
Kyle Spearrin
d2835dd577 remove angualr http and upgrade node-sass 2020-01-27 09:03:41 -05:00
Kyle Spearrin
62abc99b61 devtool false 2020-01-22 14:22:32 -05:00
Kyle Spearrin
20de62cc79 update jslib 2020-01-09 17:30:32 -05:00
Kyle Spearrin
731614279f npm i 2019-10-21 08:45:25 -04:00
Kyle Spearrin
2be751dc8e npm audit fix 2019-10-07 14:57:35 -04:00
Kyle Spearrin
e0409941a3 symlink 2019-09-25 16:10:10 -04:00
Kyle Spearrin
e6a5a3c8c1 update jslib 2019-08-30 14:21:52 -04:00
Kyle Spearrin
3f3590a223 update jslib 2019-08-20 13:47:52 -04:00
Kyle Spearrin
c1f64d7b82 disable-library-validation entitlement 2019-07-31 23:44:11 -04:00
Kyle Spearrin
f90611c96f update keytar 2019-07-31 23:43:34 -04:00
Kyle Spearrin
630e21f7c1 update jslib 2019-07-25 20:43:09 -04:00
Kyle Spearrin
2e81642c0e bump version 2019-07-25 14:21:04 -04:00
Kyle Spearrin
39514b9550 update jslib 2019-07-25 14:18:23 -04:00
Kyle Spearrin
2da82d5610 notarize directory connector 2019-07-25 14:17:50 -04:00
Kyle Spearrin
69f33a08b6 upgrade electron builder 2019-07-24 15:30:43 -04:00
Kyle Spearrin
a05e9c7746 upgrade to electron 5 2019-07-24 14:37:43 -04:00
Kyle Spearrin
d8031e4f49 update jslib 2019-07-10 08:39:21 -04:00
Kyle Spearrin
e6aa07ba5c plaintext secrets env variable 2019-07-05 11:57:25 -04:00
Kyle Spearrin
173129014a re-set state service on log in 2019-07-02 08:37:52 -04:00
Kyle Spearrin
8d4baa6d31 simlink for windows 2019-06-24 21:13:07 -04:00
Kyle Spearrin
20463ce653 update jslib, add node https proxy 2019-06-24 11:09:41 -04:00
Kyle Spearrin
2617f96710 bump version 2019-06-08 13:30:09 -04:00
Kyle Spearrin
53b0614faf only set domain if it has value 2019-06-08 00:15:44 -04:00
Kyle Spearrin
a01db9c448 empty string check on customer 2019-06-08 00:11:29 -04:00
Kyle Spearrin
84dc8e3696 update jslib 2019-06-07 11:22:26 -04:00
Kyle Spearrin
e7b988c042 trimLeft on gsuite key 2019-06-07 11:21:55 -04:00
Kyle Spearrin
a06212af1a write failed responses to stderr 2019-06-04 21:03:30 -04:00
Kyle Spearrin
b217ac9456 update jslib 2019-06-04 00:09:40 -04:00
Kyle Spearrin
ff8422d6c1 alwaysOnTop 2019-06-03 08:41:19 -04:00
Kyle Spearrin
b51d279ba6 menu bar on mac 2019-06-01 22:15:24 -04:00
Kyle Spearrin
78e4601413 update jslib 2019-05-27 10:31:22 -04:00
Kyle Spearrin
5900db10eb update jslib 2019-05-13 08:54:32 -04:00
Kyle Spearrin
c9bd853032 version bump 2019-05-06 21:32:56 -04:00
Kyle Spearrin
976fcad098 overwriteExisting on org user import 2019-05-06 21:32:25 -04:00
Kyle Spearrin
5d8193c348 update jslib 2019-04-18 10:11:38 -04:00
Kyle Spearrin
cc09b26818 hideTitleBar false 2019-04-13 21:37:54 -04:00
Kyle Spearrin
b0247caa4c getDeletedUsers for azure ad 2019-04-12 09:58:19 -04:00
Kyle Spearrin
38316abeae premises 2019-04-04 00:45:46 -04:00
Kyle Spearrin
0991dfa6bd update jslib 2019-04-02 11:54:07 -04:00
Kyle Spearrin
88029663e9 update jslib 2019-04-02 08:20:10 -04:00
Kyle Spearrin
3f5cb10db6 ignore add-edit component 2019-03-29 00:36:07 -04:00
Kyle Spearrin
1c927722f0 update bootstrap 2019-03-29 00:25:06 -04:00
Kyle Spearrin
c04354ba1c npm audit fix 2019-03-29 00:06:07 -04:00
Kyle Spearrin
3335659c8d bump version 2019-03-26 12:24:16 -04:00
Kyle Spearrin
f0282b33f0 add paging to members list api 2019-03-26 09:06:42 -04:00
Kyle Spearrin
f69d461463 move scope of filters for delUsers query 2019-03-26 09:02:35 -04:00
Elie Mélois
bcf16562fe Manage multiple pages when fetching users (#15) 2019-03-26 08:57:01 -04:00
Kyle Spearrin
d7412410f2 group paging cleanup 2019-03-26 08:02:09 -04:00
Elie Mélois
d5907477df Manage multiple pages when fetching groups (#14) 2019-03-26 07:58:05 -04:00
Kyle Spearrin
9acfaff361 remove echo 2019-03-22 22:25:37 -04:00
Kyle Spearrin
737274da59 test for ver info during install 2019-03-22 22:20:41 -04:00
Kyle Spearrin
7a0e0e6915 WIN_PKG set with ps 2019-03-22 22:16:00 -04:00
Kyle Spearrin
25847b9c83 ver info testing 2019-03-22 22:11:32 -04:00
Kyle Spearrin
07dd7bb93e bump version 2019-03-22 17:35:29 -04:00
Kyle Spearrin
bc9936bd92 update lock file 2019-03-22 17:29:38 -04:00
Kyle Spearrin
e4428e3387 update ldapjs fork 2019-03-22 17:21:18 -04:00
Kyle Spearrin
30ef0ce140 back to node 10 2019-03-22 16:55:51 -04:00
Kyle Spearrin
38c461ebe0 use ldapjs fork 2019-03-22 16:55:20 -04:00
Kyle Spearrin
ead1776f90 try node 9 2019-03-22 16:38:49 -04:00
Kyle Spearrin
0e944b2442 simplify sim promise 2019-03-22 13:12:52 -04:00
Kyle Spearrin
3bbd503308 RELEASE_NAME is just numbers 2019-03-20 23:09:54 -04:00
Kyle Spearrin
a7b13b168e release name fix 2019-03-20 23:09:12 -04:00
Kyle Spearrin
1cf8ae1bdf bump version 2019-03-20 22:16:31 -04:00
Kyle Spearrin
7da3c8e252 Merge branch 'master' of github.com:bitwarden/directory-connector 2019-03-20 17:33:13 -04:00
Kyle Spearrin
2edf3fb68d data-file and last-sync commands 2019-03-20 17:33:11 -04:00
Kyle Spearrin
1e698ba81b Update README.md 2019-03-20 16:44:41 -04:00
Kyle Spearrin
e9034ea6fe Update README.md 2019-03-20 16:43:48 -04:00
Kyle Spearrin
08ebc1ce16 build for dist 2019-03-20 14:54:52 -04:00
Kyle Spearrin
a80c95e1cf version test is only windows 2019-03-20 12:26:09 -04:00
Kyle Spearrin
c3ab785d66 test version script 2019-03-20 12:09:23 -04:00
Kyle Spearrin
baef6210c1 update keytar 2019-03-20 11:45:50 -04:00
Kyle Spearrin
74d4a6fa72 update jslib 2019-03-19 15:53:23 -04:00
Kyle Spearrin
770e41a657 bump version 2019-03-19 09:07:51 -04:00
Kyle Spearrin
d04eb03b3c node types to v10 2019-03-18 14:55:09 -04:00
Kyle Spearrin
9a8224a8d6 config different settings 2019-03-18 14:18:53 -04:00
Kyle Spearrin
bd072b727c clear cache command 2019-03-18 13:04:23 -04:00
Kyle Spearrin
93cedec32c BWCLI_DEBUG for logging 2019-03-18 10:40:47 -04:00
Kyle Spearrin
3d8933e5f0 login and logout commands 2019-03-18 10:34:11 -04:00
Kyle Spearrin
790f5a5f2f properly extract and untar keytar.node 2019-03-18 09:10:13 -04:00
Kyle Spearrin
36bb24d580 enable linux builds 2019-03-16 23:47:05 -04:00
Kyle Spearrin
3e08ba95cf fixes 2019-03-16 23:45:58 -04:00
Kyle Spearrin
e50f82175b include keytar.node prebuilds 2019-03-16 23:30:53 -04:00
Kyle Spearrin
5907c8b5ce macos artifact fix 2019-03-16 22:25:47 -04:00
Kyle Spearrin
9f9b5beff9 fix deploy regex 2019-03-16 22:17:28 -04:00
Kyle Spearrin
71dbd31477 dist-cli paths 2019-03-16 22:15:19 -04:00
Kyle Spearrin
9e4d17cc7c fix PACKAGE_VERSION env variable 2019-03-16 22:07:58 -04:00
Kyle Spearrin
2ae14913cc install checksum 2019-03-16 22:02:38 -04:00
Kyle Spearrin
a4e23eee83 use cmd and sh 2019-03-16 21:53:57 -04:00
Kyle Spearrin
0bc406b332 appveyor.yml build 2019-03-16 21:42:11 -04:00
Kyle Spearrin
76a6b2407e remove pkg and keytar check 2019-03-16 20:57:17 -04:00
Kyle Spearrin
f371067d56 debug 2019-03-16 20:43:38 -04:00
Kyle Spearrin
d1507dffca check for keytar.node in same dir when packaged 2019-03-16 16:44:33 -04:00
Kyle Spearrin
c11cc0b979 pkg directory connector cli 2019-03-16 11:58:27 -04:00
Kyle Spearrin
7cc941cc84 base cli program and sync command 2019-03-16 11:27:16 -04:00
Kyle Spearrin
a847339d72 filter info logs on CLI 2019-03-16 10:47:25 -04:00
Kyle Spearrin
46827cdaa0 test command for cli 2019-03-16 00:45:17 -04:00
Kyle Spearrin
183b54f9b8 stringify and parse json 2019-03-16 00:38:41 -04:00
Kyle Spearrin
e51a70f7b9 added missing runs 2019-03-15 23:09:02 -04:00
Kyle Spearrin
f83e97abfc rebuild and reset 2019-03-15 23:06:37 -04:00
Kyle Spearrin
92a566e73e fix repo url 2019-03-15 22:39:05 -04:00
Kyle Spearrin
59ade0d2ba stub out cli for directory connector 2019-03-15 22:38:02 -04:00
Kyle Spearrin
7da368d555 remove user deltas from azure ad 2019-03-15 12:01:11 -04:00
Kyle Spearrin
e00fd648a6 move to electron-store 2019-03-11 22:45:27 -04:00
Kyle Spearrin
f74c8420fb update jslib 2019-03-06 08:45:36 -05:00
Kyle Spearrin
d2fbb8aba9 update jslib 2019-03-02 15:08:46 -05:00
Kyle Spearrin
ba8aee2148 format html files 2019-02-21 16:54:24 -05:00
Kyle Spearrin
018dfd8865 null checks 2019-02-14 15:57:52 -05:00
Kyle Spearrin
be40ee3100 update to angular 7 2019-01-25 15:23:09 -05:00
Kyle Spearrin
35f9b5ed17 update jslib 2019-01-20 23:27:28 -05:00
Kyle Spearrin
8c4450246b update jslib 2019-01-03 10:24:47 -05:00
Kyle Spearrin
537e7d10e6 update jslib 2018-12-31 13:09:08 -05:00
Kyle Spearrin
124c7288b9 zxcvbn types 2018-12-26 15:14:46 -05:00
Kyle Spearrin
3224648bc7 update electron 2018-12-26 15:11:28 -05:00
Kyle Spearrin
9b8a11b65e ignore index files 2018-12-18 22:23:56 -05:00
Kyle Spearrin
6b9d72f4a2 ignore password generator components 2018-12-18 22:10:50 -05:00
Kyle Spearrin
8d50697345 exclude some stuff 2018-12-18 17:31:09 -05:00
Kyle Spearrin
f8e9d2b73a duo_web_sdk fix 2018-12-18 17:19:52 -05:00
Kyle Spearrin
99ed78a2e6 install and use duo_web_sdk w/ npm 2018-12-18 17:00:43 -05:00
Kyle Spearrin
346dab2c26 update jslib 2018-12-13 11:30:24 -05:00
Kyle Spearrin
28582b2b0c update gulp to 4.0.0 2018-11-27 12:44:23 -05:00
Kyle Spearrin
2bda7e5f65 pass project name to updater 2018-11-27 08:32:40 -05:00
Kyle Spearrin
8849f6c513 ignore passwordGeneration.service.ts abstraction 2018-11-24 09:12:50 -05:00
Kyle Spearrin
6d50856c07 ignore password generation service 2018-11-23 16:08:35 -05:00
Kyle Spearrin
bdec0f9493 update jslib 2018-11-23 09:45:19 -05:00
Kyle Spearrin
ab31beb1ae update jslib 2018-11-15 15:45:17 -05:00
Kyle Spearrin
014d995284 update deps 2018-11-09 22:38:27 -05:00
Kyle Spearrin
69783d3606 update jslib 2018-11-09 22:37:36 -05:00
Kyle Spearrin
55a0078332 fix lock file 2018-11-07 12:34:29 -05:00
Kyle Spearrin
346e0194d4 update jslib 2018-11-07 12:33:32 -05:00
Kyle Spearrin
512f222819 update jslib 2018-11-07 12:11:10 -05:00
Kyle Spearrin
006c6b3312 update jslib 2018-11-06 16:06:38 -05:00
Kyle Spearrin
8c80a52ce2 update jslib 2018-11-06 09:05:42 -05:00
Kyle Spearrin
5d8bf54f2f update jslib 2018-10-29 13:55:45 -04:00
Kyle Spearrin
70c1eb0623 formatting 2018-10-26 07:34:16 -04:00
Jan Hajek
447b674469 Add support for filtering users based on their group membership (#9)
* Add support for filtering users based on their group membership.

* Fix async call in if statement and proper keyword detection.

* Handle case where the checkMemberGroups was failing due to deleted user.

* Pass UseEntry into the filter and simplify filter condition.

* Revert changes in package-lock.json
2018-10-26 07:29:58 -04:00
76 changed files with 12663 additions and 5581 deletions

29
.github/scripts/decrypt-secret.ps1 vendored Normal file
View File

@@ -0,0 +1,29 @@
param (
[Parameter(Mandatory=$true)]
[string] $filename,
[string] $output
)
$homePath = Resolve-Path "~" | Select-Object -ExpandProperty Path
$rootPath = $env:GITHUB_WORKSPACE
$secretInputPath = $rootPath + "/.github/secrets"
$input = $secretInputPath + "/" + $filename
$passphrase = $env:DECRYPT_FILE_PASSWORD
$secretOutputPath = $homePath + "/secrets"
if ([string]::IsNullOrEmpty($output)) {
if ($filename.EndsWith(".gpg")) {
$output = $secretOutputPath + "/" + $filename.TrimEnd(".gpg")
} else {
$output = $secretOutputPath + "/" + $filename + ".plaintext"
}
}
if (!(Test-Path -Path $secretOutputPath))
{
New-Item -ItemType Directory -Path $secretOutputPath
}
gpg --quiet --batch --yes --decrypt --passphrase="$passphrase" --output $output $input

5
.github/scripts/load-version.ps1 vendored Normal file
View File

@@ -0,0 +1,5 @@
$rootPath = $env:GITHUB_WORKSPACE;
$packageVersion = (Get-Content -Raw -Path $rootPath\src\package.json | ConvertFrom-Json).version;
Write-Output "Setting package version to $packageVersion";
Write-Output "PACKAGE_VERSION=$packageVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append;

View File

@@ -0,0 +1,7 @@
$rootPath = $env:GITHUB_WORKSPACE;
$decryptSecretPath = $($rootPath + "/.github/scripts/decrypt-secret.ps1");
Invoke-Expression "& `"$decryptSecretPath`" -filename devid-app-cert.p12.gpg"
Invoke-Expression "& `"$decryptSecretPath`" -filename devid-installer-cert.p12.gpg"
Invoke-Expression "& `"$decryptSecretPath`" -filename macdev-cert.p12.gpg"

View File

@@ -0,0 +1,15 @@
$homePath = Resolve-Path "~" | Select-Object -ExpandProperty Path;
$secretsPath = $homePath + "/secrets"
$devidAppCertPath = $($secretsPath + "/devid-app-cert.p12");
$devidInstallerCertPath = $($secretsPath + "/devid-installer-cert.p12");
$macdevCertPath = $($secretsPath + "/macdev-cert.p12");
security create-keychain -p $env:KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p $env:KEYCHAIN_PASSWORD build.keychain
security set-keychain-settings -lut 1200 build.keychain
security import $devidAppCertPath -k build.keychain -P $env:DEVID_CERT_PASSWORD -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
security import $devidInstallerCertPath -k build.keychain -P $env:DEVID_CERT_PASSWORD -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
security import $macdevCertPath -k build.keychain -P $env:MACDEV_CERT_PASSWORD -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $env:KEYCHAIN_PASSWORD build.keychain

BIN
.github/secrets/devid-app-cert.p12.gpg vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
.github/secrets/macdev-cert.p12.gpg vendored Normal file

Binary file not shown.

390
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,390 @@
name: Build
on:
push:
branches-ignore:
- 'l10n_master'
workflow_dispatch:
inputs:
jobs:
cloc:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Set up cloc
run: |
sudo apt update
sudo apt -y install cloc
- name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
setup:
runs-on: ubuntu-latest
outputs:
package_version: ${{ steps.get_version.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Get Package Version
id: get_version
shell: pwsh
run: |
$env:pkgVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).version
echo "::set-output name=PACKAGE_VERSION::$env:pkgVersion"
cli:
runs-on: windows-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Setup Windows builder
run: |
choco install checksum --no-progress
choco install reshack --no-progress
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- 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\v2.5\fetched-v10.4.1-win-x64
- name: get pkg-fetch
shell: pwsh
run: |
cd $HOME
$fetchedUrl = "https://github.com/vercel/pkg-fetch/releases/download/v2.5/uploaded-v2.5-node-v10.4.1-win-x64"
New-Item -ItemType directory -Path ./.pkg-cache
New-Item -ItemType directory -Path ./.pkg-cache/v2.5
Invoke-RestMethod -Uri $fetchedUrl -OutFile "./.pkg-cache/v2.5/fetched-v10.4.1-win-x64"
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v2.5\fetched-v10.4.1-win-x64
- name: Keytar
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"
$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
shell: pwsh
run: ./scripts/make-versioninfo.ps1
- name: Resource Hacker
shell: cmd
run: |
set PATH=%PATH%;C:\Program Files (x86)\Resource Hacker
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action delete -mask ICONGROUP,1,
ResourceHacker -open version-info.rc -save version-info.res -action compile
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action addoverwrite -resource version-info.res
- name: Install
run: npm install
- name: Package CLI
run: npm run dist:cli
- 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: |
Expand-Archive -Path "./dist-cli/bwdc-windows-${env:PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
$testVersion = Invoke-Expression '& ./test/windows/bwdc.exe -v'
echo "version: $env:PACKAGE_VERSION"
echo "testVersion: $testVersion"
if($testVersion -ne $env:PACKAGE_VERSION) {
Throw "Version test failed."
}
- name: Create checksums
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
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-windows-${{ env.PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env.PACKAGE_VERSION }}.zip
- name: Upload mac zip to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-macos-${{ env.PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env.PACKAGE_VERSION }}.zip
- name: Upload linux zip to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-linux-${{ env.PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env.PACKAGE_VERSION }}.zip
- name: Upload windows checksum to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-windows-sha256-${{ env.PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-windows-sha256-${{ env.PACKAGE_VERSION }}.txt
- name: Upload mac checksum to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-macos-sha256-${{ env.PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-macos-sha256-${{ env.PACKAGE_VERSION }}.txt
- name: Upload linux checksum to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-linux-sha256-${{ env.PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-linux-sha256-${{ env.PACKAGE_VERSION }}.txt
windows_gui:
runs-on: windows-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Set up dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
shell: pwsh
- name: Print environment
run: |
node --version
npm --version
dotnet --version
- name: Install AST
shell: pwsh
run: |
cd $HOME
git clone https://github.com/vcsjones/AzureSignTool.git
cd AzureSignTool
$latest_head = $(git rev-parse HEAD)[0..9] -join ""
$latest_version = "0.0.0-g$latest_head"
Write-Host "--------"
Write-Host "git commit - $(git rev-parse HEAD)"
Write-Host "latest_head - $latest_head"
Write-Host "PACKAGE VERSION TO BUILD - $latest_version"
Write-Host "--------"
dotnet restore
dotnet pack --output ./nupkg
dotnet tool install --global --ignore-failed-sources --add-source ./nupkg --version $latest_version azuresigntool
- name: Checkout repo
uses: actions/checkout@v2
- name: Install Node dependencies
run: npm install
- name: Run linter
run: npm run lint
- name: Build & Sign
run: npm run dist:win
env:
ELECTRON_BUILDER_SIGN: 1
SIGNING_VAULT_URL: ${{ secrets.SIGNING_VAULT_URL }}
SIGNING_CLIENT_ID: ${{ secrets.SIGNING_CLIENT_ID }}
SIGNING_TENANT_ID: ${{ secrets.SIGNING_TENANT_ID }}
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
- name: List Dist
run: dir ./dist
- name: Publish Portable Exe to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: Bitwarden-Connector-Portable-${{ env.PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env.PACKAGE_VERSION }}.exe
- name: Publish Installer Exe to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: Bitwarden-Connector-Installer-${{ env.PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env.PACKAGE_VERSION }}.exe
linux:
runs-on: ubuntu-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
- name: Set up environment
run: |
sudo apt-get update
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev
sudo apt-get -y install rpm
- name: Checkout repo
uses: actions/checkout@v2
- name: npm install
run: npm install
- name: npm rebuild
run: npm run rebuild
- name: npm package
run: npm run dist:lin
- name: Publish AppImage
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: Bitwarden-Connector-${{ env.PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env.PACKAGE_VERSION }}-x86_64.AppImage
macos:
runs-on: macos-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
- name: Print environment
run: |
node --version
npm --version
Write-Output "GitHub ref: $env:GITHUB_REF"
Write-Output "GitHub event: $env:GITHUB_EVENT"
shell: pwsh
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Checkout repo
uses: actions/checkout@v2
- name: Decrypt secrets
run: ./.github/scripts/macos/decrypt-secrets.ps1
shell: pwsh
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
- name: Set up keychain
run: ./.github/scripts/macos/setup-keychain.ps1
shell: pwsh
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
DEVID_CERT_PASSWORD: ${{ secrets.DEVID_CERT_PASSWORD }}
MACDEV_CERT_PASSWORD: ${{ secrets.MACDEV_CERT_PASSWORD }}
- name: Load package version
run: ./.github/scripts/load-version.ps1
shell: pwsh
- name: Install Node dependencies
run: npm install
- name: Run linter
run: npm run lint
- name: Build application (dev)
if: github.ref != 'refs/heads/master'
run: npm run build
- name: Build application (dist)
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
run: npm run dist:mac
env:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
- name: Upload .zip artifact
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: Bitwarden-Connector-${{ env.PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env.PACKAGE_VERSION }}-mac.zip
- name: Upload .dmg artifact
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: Bitwarden-Connector-${{ env.PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env.PACKAGE_VERSION }}.dmg

392
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,392 @@
name: Release
on:
workflow_dispatch:
inputs:
release_tag_name_input:
description: "Release Tag Name <X.X.X>"
required: true
jobs:
setup:
runs-on: ubuntu-latest
outputs:
package_version: ${{ steps.create_tags.outputs.package_version }}
tag_version: ${{ steps.create_tags.outputs.tag_version }}
release_upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Create Release Vars
id: create_tags
run: |
case "${RELEASE_TAG_NAME_INPUT:0:1}" in
v)
echo "RELEASE_NAME=${RELEASE_TAG_NAME_INPUT:1}" >> $GITHUB_ENV
echo "RELEASE_TAG_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "::set-output name=package_version::${RELEASE_TAG_NAME_INPUT:1}"
echo "::set-output name=tag_version::$RELEASE_TAG_NAME_INPUT"
;;
[0-9])
echo "RELEASE_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "RELEASE_TAG_NAME=v$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "::set-output name=package_version::$RELEASE_TAG_NAME_INPUT"
echo "::set-output name=tag_version::v$RELEASE_TAG_NAME_INPUT"
;;
*)
exit 1
;;
esac
env:
RELEASE_TAG_NAME_INPUT: ${{ github.event.inputs.release_tag_name_input }}
- name: Create Draft Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.RELEASE_TAG_NAME }}
release_name: ${{ env.RELEASE_NAME }}
draft: true
prerelease: false
cli:
runs-on: windows-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Setup Windows builder
run: |
choco install checksum --no-progress
choco install reshack --no-progress
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Set VER_INFO
run: |
echo "WIN_PKG=$env:WIN_PKG" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v2.5\fetched-v10.4.1-win-x64
- name: get pkg-fetch
shell: pwsh
run: |
cd $HOME
$fetchedUrl = "https://github.com/vercel/pkg-fetch/releases/download/v2.5/uploaded-v2.5-node-v10.4.1-win-x64"
New-Item -ItemType directory -Path ./.pkg-cache
New-Item -ItemType directory -Path ./.pkg-cache/v2.5
Invoke-RestMethod -Uri $fetchedUrl -OutFile "./.pkg-cache/v2.5/fetched-v10.4.1-win-x64"
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v2.5\fetched-v10.4.1-win-x64
- name: Keytar
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"
$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
shell: pwsh
run: ./scripts/make-versioninfo.ps1
- name: Resource Hacker
shell: cmd
run: |
set PATH=%PATH%;C:\Program Files (x86)\Resource Hacker
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action delete -mask ICONGROUP,1,
ResourceHacker -open version-info.rc -save version-info.res -action compile
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action addoverwrite -resource version-info.res
- name: Install
run: npm install
- name: Package CLI
run: npm run dist:cli
- 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: |
Expand-Archive -Path "./dist-cli/bwdc-windows-${env:PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
$testVersion = Invoke-Expression '& ./test/windows/bwdc.exe -v'
echo "version: $env:PACKAGE_VERSION"
echo "testVersion: $testVersion"
if($testVersion -ne $env:PACKAGE_VERSION) {
Throw "Version test failed."
}
- name: Create checksums
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 release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-windows-${{ env.PACKAGE_VERSION }}.zip
asset_name: bwdc-windows-${{ env.PACKAGE_VERSION }}.zip
asset_content_type: application/zip
- name: upload macos zip release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-macos-${{ env.PACKAGE_VERSION }}.zip
asset_name: bwdc-macos-${{ env.PACKAGE_VERSION }}.zip
asset_content_type: application/zip
- name: upload linux zip release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-linux-${{ env.PACKAGE_VERSION }}.zip
asset_name: bwdc-linux-${{ env.PACKAGE_VERSION }}.zip
asset_content_type: application/zip
- name: upload windows checksum release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-windows-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_name: bwdc-windows-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_content_type: text/plain
- name: upload macos checksum release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-macos-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_name: bwdc-macos-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_content_type: text/plain
- name: upload linux checksum release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-linux-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_name: bwdc-linux-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_content_type: text/plain
windows-gui:
runs-on: windows-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Set up dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
shell: pwsh
- name: Print environment
run: |
node --version
npm --version
dotnet --version
- name: Install AST
shell: pwsh
run: |
cd $HOME
git clone https://github.com/vcsjones/AzureSignTool.git
cd AzureSignTool
$latest_head = $(git rev-parse HEAD)[0..9] -join ""
$latest_version = "0.0.0-g$latest_head"
Write-Host "--------"
Write-Host "git commit - $(git rev-parse HEAD)"
Write-Host "latest_head - $latest_head"
Write-Host "PACKAGE VERSION TO BUILD - $latest_version"
Write-Host "--------"
dotnet restore
dotnet pack --output ./nupkg
dotnet tool install --global --ignore-failed-sources --add-source ./nupkg --version $latest_version azuresigntool
cd $HOME
- name: Checkout repo
uses: actions/checkout@v2
- name: Install Node dependencies
run: npm install
- name: Run linter
run: npm run lint
- name: npm rebuild
run: npm run rebuild
- name: Build & Sign
run: |
npm run publish:win
env:
ELECTRON_BUILDER_SIGN: 1
SIGNING_VAULT_URL: ${{ secrets.SIGNING_VAULT_URL }}
SIGNING_CLIENT_ID: ${{ secrets.SIGNING_CLIENT_ID }}
SIGNING_TENANT_ID: ${{ secrets.SIGNING_TENANT_ID }}
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
linux:
runs-on: ubuntu-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
- name: Set up environment
run: |
sudo apt-get update
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev
sudo apt-get -y install rpm
- name: Checkout repo
uses: actions/checkout@v2
- name: Set PACKAGE_VERSION
shell: pwsh
run: |
$env:pkgVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).version
echo "PACKAGE_VERSION=$env:pkgVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "version: $env:pkgVersion"
- name: npm install
run: npm install
- name: npm rebuild
run: npm run rebuild
- name: npm package
run: npm run publish:lin
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
macos:
runs-on: macos-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
- name: Print environment
run: |
node --version
npm --version
Write-Output "GitHub ref: $env:GITHUB_REF"
Write-Output "GitHub event: $env:GITHUB_EVENT"
shell: pwsh
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Checkout repo
uses: actions/checkout@v2
- name: Decrypt secrets
run: ./.github/scripts/macos/decrypt-secrets.ps1
shell: pwsh
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
- name: Set up keychain
run: ./.github/scripts/macos/setup-keychain.ps1
shell: pwsh
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
DEVID_CERT_PASSWORD: ${{ secrets.DEVID_CERT_PASSWORD }}
MACDEV_CERT_PASSWORD: ${{ secrets.MACDEV_CERT_PASSWORD }}
- name: Load package version
run: ./.github/scripts/load-version.ps1
shell: pwsh
- name: Install Node dependencies
run: npm install
- name: Run linter
run: npm run lint
- name: Build application (dist)
run: npm run publish:mac
env:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View File

@@ -4,11 +4,14 @@ node_modules
npm-debug.log
vwd.webinfo
dist/
dist-cli/
css/
*.crx
*.pem
build-cli/
build/
yarn-error.log
.DS_Store
*.nupkg
*.provisionprofile
*.env

13
.vscode/launch.json vendored
View File

@@ -23,7 +23,18 @@
"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": [
{

View File

@@ -1,4 +1,4 @@
[![appveyor build](https://ci.appveyor.com/api/projects/status/github/bitwarden/directory-connector?branch=master&svg=true)](https://ci.appveyor.com/project/bitwarden/directory-connector)
![Build](https://github.com/bitwarden/directory-connector/workflows/Build/badge.svg)
[![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
# Bitwarden Directory Connector
@@ -18,7 +18,32 @@ The application is written using Electron with Angular and installs on Windows,
![Directory Connector](https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/directory-connector-macos.png "Dashboard")
# Build/Run
## Command-line Interface
A command-line interface tool is also available for the Bitwarden Directory Connector. The Directory Connector CLI (`bwdc`) is written with TypeScript and Node.js and can also be run on Windows, macOS, and Linux distributions.
## CLI Documentation
The Bitwarden Directory Connector CLI is self-documented with `--help` content and examples for every command. You should start exploring the CLI by using the global `--help` option:
```bash
bwdc --help
```
This option will list all available commands that you can use with the Directory Connector CLI.
Additionally, you can run the `--help` option on a specific command to learn more about it:
```
bwdc test --help
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.
## Build/Run
**Requirements**
@@ -29,10 +54,26 @@ The application is written using Electron with Angular and installs on Windows,
```bash
npm install
npm run reset # Only necessary if you have previously run the CLI app
npm run rebuild
npm run electron
```
# Contribute
**Run the CLI**
```bash
npm install
npm run reset # Only necessary if you have previously run the desktop app
npm run build:cli:watch
```
You can then run commands from the `./build-cli` folder:
```bash
node ./build-cli/bwdc.js --help
```
## 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.

View File

@@ -25,7 +25,7 @@ function cleanupAotIssue() {
return del(['./node_modules/@types/uglify-js/node_modules/source-map/source-map.d.ts']);
}
gulp.task('clean', clean);
gulp.task('cleanupAotIssue', cleanupAotIssue);
gulp.task('webfonts', ['clean'], webfonts);
gulp.task('prebuild:renderer', ['webfonts', 'cleanupAotIssue']);
exports.clean = clean;
exports.cleanupAotIssue = cleanupAotIssue;
exports.webfonts = gulp.series(clean, webfonts);
exports['prebuild:renderer'] = gulp.parallel(webfonts, cleanupAotIssue);;

2
jslib

Submodule jslib updated: 2f6426deb4...8541027d40

13808
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
"vault",
"password manager"
],
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"repository": {
"type": "git",
@@ -21,37 +21,61 @@
"sub:update": "git submodule update --remote",
"sub:pull": "git submodule foreach git pull origin master",
"sub:commit": "npm run sub:pull && git commit -am \"update submodule\"",
"postinstall": "./node_modules/.bin/electron-rebuild && npm run sub:init",
"lint": "tslint src/**/*.ts || true",
"lint:fix": "tslint src/**/*.ts --fix",
"postinstall": "npm run sub:init",
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
"symlink:mac": "npm run symlink:lin",
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"rebuild": "./node_modules/.bin/electron-rebuild",
"reset": "rimraf ./node_modules/keytar/* && npm install",
"lint": "tslint 'src/**/*.ts' || true",
"lint:fix": "tslint 'src/**/*.ts' --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": "gulp prebuild:renderer && webpack --config webpack.renderer.js",
"build:renderer:watch": "gulp prebuild:renderer && webpack --config webpack.renderer.js --watch",
"build:dist": "npm run reset && npm run rebuild && npm run build",
"build:cli": "webpack --config webpack.cli.js",
"build:cli:watch": "webpack --config webpack.cli.js --watch",
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.js",
"build:cli:prod:watch": "cross-env NODE_ENV=production webpack --config webpack.cli.js --watch",
"electron": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 ./build --watch\" \"npm run build:renderer:watch\"",
"clean:dist": "rimraf ./dist/*",
"pack:lin": "npm run clean:dist && build --linux --x64 -p never",
"pack:mac": "npm run clean:dist && build --mac -p never",
"pack:win": "npm run clean:dist && build --win --x64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
"pack:win:ci": "npm run clean:dist && build --win --x64 --ia32 -p never",
"dist:lin": "npm run build && npm run pack:lin",
"dist:mac": "npm run build && npm run pack:mac",
"dist:win": "npm run build && npm run pack:win",
"clean:dist:cli": "rimraf ./dist-cli/*",
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never",
"pack:mac": "npm run clean:dist && electron-builder --mac -p never",
"pack:win": "npm run clean:dist && electron-builder --win --x64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
"pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --ia32 -p never",
"pack:cli": "npm run pack:cli:win | npm run pack:cli:mac | npm run pack:cli:lin",
"pack:cli:win": "pkg ./src-cli --targets win-x64 --output ./dist-cli/windows/bwdc.exe",
"pack:cli:mac": "pkg ./src-cli --targets macos-x64 --output ./dist-cli/macos/bwdc",
"pack:cli:lin": "pkg ./src-cli --targets linux-x64 --output ./dist-cli/linux/bwdc",
"dist:lin": "npm run build:dist && npm run pack:lin",
"dist:mac": "npm run build:dist && npm run pack:mac",
"dist:win": "npm run build:dist && npm run pack:win",
"dist:win:ci": "npm run build && npm run pack:win:ci",
"publish:lin": "npm run build && npm run clean:dist && build --linux --x64 -p always",
"publish:mac": "npm run build && npm run clean:dist && build --mac -p always",
"publish:win": "npm run build && npm run clean:dist && build --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\""
"dist:cli": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli",
"dist:cli:win": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:win",
"dist:cli:mac": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:mac",
"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\""
},
"build": {
"appId": "com.bitwarden.directory-connector",
"copyright": "Copyright © 2015-2018 8bit Solutions LLC",
"copyright": "Copyright © 2015-2020 Bitwarden Inc.",
"directories": {
"buildResources": "resources",
"output": "dist",
"app": "build"
},
"afterSign": "scripts/notarize.js",
"mac": {
"category": "public.app-category.productivity",
"gatekeeperAssess": false,
"hardenedRuntime": true,
"entitlements": "resources/entitlements.mac.plist",
"entitlementsInherit": "resources/entitlements.mac.plist",
"target": [
"dmg",
"zip"
@@ -61,7 +85,8 @@
"target": [
"portable",
"nsis"
]
],
"sign": "scripts/sign.js"
},
"linux": {
"category": "Utility",
@@ -107,73 +132,96 @@
}
},
"devDependencies": {
"@angular/compiler-cli": "^6.1.7",
"@angular/compiler-cli": "^9.1.12",
"@microsoft/microsoft-graph-types": "^1.4.0",
"@ngtools/webpack": "^6.2.1",
"@ngtools/webpack": "^9.1.12",
"@types/commander": "^2.12.2",
"@types/form-data": "^2.2.1",
"@types/inquirer": "^0.0.43",
"@types/ldapjs": "^1.0.3",
"@types/lowdb": "^1.0.5",
"@types/lunr": "^2.1.6",
"@types/lunr": "^2.3.3",
"@types/node": "^10.17.28",
"@types/node-fetch": "^2.1.2",
"@types/node-forge": "^0.7.5",
"@types/papaparse": "^4.5.3",
"@types/proper-lockfile": "^4.1.1",
"@types/semver": "^5.5.0",
"@types/source-map": "0.5.2",
"@types/webcrypto": "^0.0.28",
"@types/webpack": "^4.4.11",
"@types/zxcvbn": "4.4.0",
"clean-webpack-plugin": "^0.1.19",
"concurrently": "^4.0.1",
"copy-webpack-plugin": "^4.5.2",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"del": "^3.0.0",
"electron": "2.0.11",
"electron-builder": "^20.25.0",
"electron-rebuild": "1.8.1",
"electron-reload": "1.2.5",
"extract-text-webpack-plugin": "next",
"electron": "11.1.1",
"electron-builder": "22.9.1",
"electron-notarize": "^1.0.0",
"electron-rebuild": "^2.3.4",
"electron-reload": "^1.5.0",
"file-loader": "^2.0.0",
"font-awesome": "4.7.0",
"gulp": "^3.9.1",
"gulp": "^4.0.0",
"gulp-google-webfonts": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.9.0",
"node-abi": "^2.9.0",
"node-loader": "^0.6.0",
"node-sass": "^4.9.3",
"node-sass": "^4.13.1",
"pkg": "4.3.4",
"rimraf": "^2.6.2",
"sass-loader": "^7.1.0",
"ts-loader": "^5.1.0",
"tslint": "^5.11.0",
"tslint-loader": "^3.6.0",
"typescript": "^2.7.2",
"webpack": "^4.18.0",
"webpack-cli": "^3.1.0",
"webpack-merge": "^4.1.4",
"ts-loader": "^7.0.5",
"tslint": "^5.12.1",
"tslint-loader": "^3.5.4",
"typescript": "3.8.3",
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1",
"webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"@angular/animations": "6.1.7",
"@angular/common": "6.1.7",
"@angular/compiler": "6.1.7",
"@angular/core": "6.1.7",
"@angular/forms": "6.1.7",
"@angular/http": "6.1.7",
"@angular/platform-browser": "6.1.7",
"@angular/platform-browser-dynamic": "6.1.7",
"@angular/router": "6.1.7",
"@angular/upgrade": "6.1.7",
"@angular/animations": "9.1.12",
"@angular/common": "9.1.12",
"@angular/compiler": "9.1.12",
"@angular/core": "9.1.12",
"@angular/forms": "9.1.12",
"@angular/platform-browser": "9.1.12",
"@angular/platform-browser-dynamic": "9.1.12",
"@angular/router": "9.1.12",
"@angular/upgrade": "9.1.12",
"@microsoft/microsoft-graph-client": "1.2.0",
"@okta/okta-sdk-nodejs": "1.2.0",
"angular2-toaster": "6.1.0",
"angulartics2": "6.3.0",
"bootstrap": "4.1.3",
"core-js": "2.5.7",
"electron-log": "2.2.14",
"electron-updater": "3.0.3",
"googleapis": "33.0.0",
"keytar": "4.2.1",
"ldapjs": "1.0.2",
"angular2-toaster": "8.0.0",
"angulartics2": "9.1.0",
"big-integer": "1.6.36",
"bootstrap": "4.3.1",
"browser-hrtime": "^1.1.8",
"chalk": "2.4.1",
"commander": "7.0.0",
"core-js": "2.6.2",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"electron-log": "4.3.0",
"electron-store": "6.0.1",
"electron-updater": "4.3.5",
"form-data": "2.3.2",
"googleapis": "43.0.0",
"https-proxy-agent": "5.0.0",
"inquirer": "6.2.0",
"keytar": "7.3.0",
"ldapjs": "git+https://git@github.com/kspearrin/node-ldapjs.git",
"lowdb": "1.0.0",
"lunr": "2.3.3",
"node-fetch": "2.2.0",
"node-forge": "0.7.6",
"rxjs": "6.3.2",
"zone.js": "0.8.26"
"open": "7.1.0",
"proper-lockfile": "^4.1.2",
"rxjs": "6.6.2",
"tslib": "^2.0.1",
"zone.js": "0.10.3",
"zxcvbn": "4.4.2"
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,33 @@
$major,$minor,$patch = $env:PACKAGE_VERSION.split('.')
$versionInfo = @"
1 VERSIONINFO
FILEVERSION $major,$minor,$patch,0
PRODUCTVERSION $major,$minor,$patch,0
FILEOS 0x40004
FILETYPE 0x1
{
BLOCK "StringFileInfo"
{
BLOCK "040904b0"
{
VALUE "CompanyName", "Bitwarden Inc."
VALUE "ProductName", "Bitwarden"
VALUE "FileDescription", "Bitwarden Directory Connector CLI"
VALUE "FileVersion", "$env:PACKAGE_VERSION"
VALUE "ProductVersion", "$env:PACKAGE_VERSION"
VALUE "OriginalFilename", "bwdc.exe"
VALUE "InternalName", "bwdc"
VALUE "LegalCopyright", "Copyright Bitwarden Inc."
}
}
BLOCK "VarFileInfo"
{
VALUE "Translation", 0x0409 0x04B0
}
}
"@
$versionInfo | Out-File ./version-info.rc

18
scripts/notarize.js Normal file
View File

@@ -0,0 +1,18 @@
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,
});
};

23
scripts/sign.js Normal file
View File

@@ -0,0 +1,23 @@
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}"`,
{
stdio: "inherit"
}
);
}
};

24
src-cli/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "bitwarden-directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2.9.0",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
"main": "main.js",
"repository": {
"type": "git",
"url": "https://github.com/bitwarden/directory-connector"
},
"bin": {
"bwdc": "../build-cli/bwdc.js"
},
"pkg": {
"assets": "../build-cli/**/*"
},
"dependencies": {
"browser-hrtime": "^1.1.8",
"keytar": "7.3.0"
}
}

View File

@@ -17,13 +17,19 @@
</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">
<input id="identityUrl" type="text" name="IdentityUrl" [(ngModel)]="identityUrl"
class="form-control">
</div>
</div>
<div class="modal-footer justify-content-start">

View File

@@ -14,17 +14,26 @@
<div class="form-group">
<div class="row-main">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPassword" [(ngModel)]="masterPassword" class="form-control">
<input id="masterPassword" type="password" name="MasterPassword"
[(ngModel)]="masterPassword" class="form-control">
</div>
</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>
<button type="button" class="btn btn-link" (click)="settings()">
{{'settings' | i18n}}
</button>
<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>
<button type="button" class="btn btn-secondary ml-1" (click)="sso()">
<i class="fa fa-bank" aria-hidden="true"></i>
{{'enterpriseSingleSignOn' | i18n}}
</button>
</div>
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
{{'settings' | i18n}}
</button>
</div>
</div>
</div>
</div>

View File

@@ -9,8 +9,12 @@ import { Router } from '@angular/router';
import { EnvironmentComponent } from './environment.component';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/login.component';
@@ -21,12 +25,18 @@ import { ModalComponent } from 'jslib/angular/components/modal.component';
templateUrl: 'login.component.html',
})
export class LoginComponent extends BaseLoginComponent {
@ViewChild('environment', { read: ViewContainerRef }) environmentModal: ViewContainerRef;
@ViewChild('environment', { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef;
constructor(authService: AuthService, router: Router,
i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
storageService: StorageService, platformUtilsService: PlatformUtilsService) {
super(authService, router, platformUtilsService, i18nService, storageService);
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
super(authService, router,
platformUtilsService, i18nService,
stateService, environmentService,
passwordGenerationService, cryptoFunctionService,
storageService);
super.successRoute = '/tabs/dashboard';
}
@@ -40,4 +50,8 @@ export class LoginComponent extends BaseLoginComponent {
modal.close();
});
}
sso() {
return super.launchSsoBrowser('connector', 'bwdc://sso-callback');
}
}

View File

@@ -0,0 +1,30 @@
<div class="container-fluid">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="card" *ngIf="!showMasterPassRedirect">
<div class="card-body">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
{{'loading' | i18n}}
</div>
</div>
<div class="card" *ngIf="showMasterPassRedirect">
<h5 class="card-header">{{'setMasterPassword' | i18n}}</h5>
<div class="card-body">
<p class="text-center">{{'setMasterPasswordRedirect' | i18n}}</p>
<hr>
<div class="d-flex">
<button type="button" class="btn btn-primary btn-block btn-submit"
(click)="launchWebVault()">
{{'launchWebVault' | i18n}}
</button>
<button type="button" class="btn btn-secondary btn-block ml-2 mt-0" (click)="logOut()">
{{'logOut' | i18n}}
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,61 @@
import { Component } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component';
@Component({
selector: 'app-sso',
templateUrl: 'sso.component.html',
})
export class SsoComponent extends BaseSsoComponent {
showMasterPassRedirect: boolean = false;
constructor(authService: AuthService, router: Router,
i18nService: I18nService, route: ActivatedRoute,
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService, apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
passwordGenerationService: PasswordGenerationService, private messagingService: MessagingService,
private environmentService: EnvironmentService) {
super(authService, router, i18nService, route, storageService, stateService, platformUtilsService,
apiService, cryptoFunctionService, passwordGenerationService);
this.successRoute = '/tabs/dashboard';
this.redirectUri = 'bwdc://sso-callback';
this.clientId = 'connector';
this.onSuccessfulLoginChangePasswordNavigate = this.redirectSetMasterPass;
}
async redirectSetMasterPass() {
this.showMasterPassRedirect = true;
}
launchWebVault() {
const webUrl = this.environmentService.webVaultUrl == null ? 'https://vault.bitwarden.com' :
this.environmentService.webVaultUrl;
this.platformUtilsService.launchUri(webUrl);
}
async logOut() {
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('logOutConfirmation'),
this.i18nService.t('logOut'), this.i18nService.t('logOut'), this.i18nService.t('cancel'));
if (confirmed) {
this.messagingService.send('logout');
}
}
}

View File

@@ -5,7 +5,8 @@
<div class="card">
<h5 class="card-header">{{title}}</h5>
<div class="card-body">
<ng-container *ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
<ng-container
*ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
<p *ngIf="selectedProviderType === providerType.Authenticator">
{{'enterVerificationCodeApp' | i18n}}
</p>
@@ -14,7 +15,8 @@
</p>
<div class="form-group">
<label for="code">{{'verificationCode' | i18n}}</label>
<input id="code" type="text" name="Code" [(ngModel)]="token" appAutofocus class="form-control">
<input id="code" type="text" name="Code" [(ngModel)]="token" appAutofocus
class="form-control">
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
@@ -22,7 +24,8 @@
<p><img src="../../images/yubikey.jpg" class="img-fluid rounded" alt=""></p>
<div class="form-group">
<label for="code">{{'verificationCode' | i18n}}</label>
<input id="code" type="password" name="Code" [(ngModel)]="token" appAutofocus class="form-control">
<input id="code" type="password" name="Code" [(ngModel)]="token" appAutofocus
class="form-control">
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Duo ||
@@ -31,13 +34,16 @@
<iframe id="duo_iframe"></iframe>
</div>
</ng-container>
<div class="form-group" *ngIf="selectedProviderType != null && selectedProviderType !== providerType.U2f">
<div class="form-group"
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.U2f">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="remember" [(ngModel)]="remember" name="Remember">
<input class="form-check-input" type="checkbox" id="remember" [(ngModel)]="remember"
name="Remember">
<label class="form-check-label" for="remember">{{'rememberMe' | i18n}}</label>
</div>
</div>
<ng-container class="card-body" *ngIf="selectedProviderType === null || selectedProviderType === providerType.U2f">
<ng-container class="card-body"
*ngIf="selectedProviderType === null || selectedProviderType === providerType.U2f">
<p>{{'noTwoStepProviders' | i18n}}</p>
<p>{{'noTwoStepProviders2' | i18n}}</p>
</ng-container>
@@ -52,7 +58,8 @@
</div>
<div class="text-center mt-3">
<a href="#" appStopClick (click)="anotherMethod()">{{'useAnotherTwoStepMethod' | i18n}}</a>
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise" *ngIf="selectedProviderType === providerType.Email">
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise"
*ngIf="selectedProviderType === providerType.Email">
{{'sendVerificationCodeEmailAgain' | i18n}}
</a>
</div>

View File

@@ -5,7 +5,10 @@ import {
ViewContainerRef,
} from '@angular/core';
import { Router } from '@angular/router';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { TwoFactorOptionsComponent } from './two-factor-options.component';
@@ -16,6 +19,8 @@ import { AuthService } from 'jslib/abstractions/auth.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { ModalComponent } from 'jslib/angular/components/modal.component';
import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/components/two-factor.component';
@@ -25,13 +30,19 @@ import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/comp
templateUrl: 'two-factor.component.html',
})
export class TwoFactorComponent extends BaseTwoFactorComponent {
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
@ViewChild('twoFactorOptions', { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef;
constructor(authService: AuthService, router: Router,
i18nService: I18nService, apiService: ApiService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
private componentFactoryResolver: ComponentFactoryResolver) {
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService);
private componentFactoryResolver: ComponentFactoryResolver, stateService: StateService,
storageService: StorageService, route: ActivatedRoute) {
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
stateService, storageService, route);
}
async ngOnInit() {
await super.ngOnInit();
super.successRoute = '/tabs/dashboard';
}

View File

@@ -8,6 +8,7 @@ import { AuthGuardService } from './services/auth-guard.service';
import { LaunchGuardService } from './services/launch-guard.service';
import { LoginComponent } from './accounts/login.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { DashboardComponent } from './tabs/dashboard.component';
import { MoreComponent } from './tabs/more.component';
@@ -22,6 +23,7 @@ const routes: Routes = [
canActivate: [LaunchGuardService],
},
{ path: '2fa', component: TwoFactorComponent },
{ path: 'sso', component: SsoComponent },
{
path: 'tabs',
component: TabsComponent,

View File

@@ -48,7 +48,7 @@ const BroadcasterSubscriptionId = 'AppComponent';
<router-outlet></router-outlet>`,
})
export class AppComponent implements OnInit {
@ViewChild('settings', { read: ViewContainerRef }) settingsRef: ViewContainerRef;
@ViewChild('settings', { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
toasterConfig: ToasterConfig = new ToasterConfig({
showCloseButton: true,
@@ -133,6 +133,9 @@ export class AppComponent implements OnInit {
properties: { label: message.label },
});
break;
case 'ssoCallback':
this.router.navigate(['sso'], { queryParams: { code: message.code, state: message.state } });
break;
default:
}
});

View File

@@ -15,11 +15,13 @@ 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 { ModalComponent } from 'jslib/angular/components/modal.component';
import { EnvironmentComponent } from './accounts/environment.component';
import { LoginComponent } from './accounts/login.component';
import { SsoComponent } from './accounts/sso.component';
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { DashboardComponent } from './tabs/dashboard.component';
@@ -27,6 +29,7 @@ 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';
@@ -45,7 +48,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
FormsModule,
AppRoutingModule,
ServicesModule,
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
Angulartics2Module.forRoot({
pageTracking: {
clearQueryParams: true,
},
@@ -53,11 +56,13 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
ToasterModule.forRoot(),
],
declarations: [
A11yTitleDirective,
ApiActionDirective,
AppComponent,
AutofocusDirective,
BlurClickDirective,
BoxRowDirective,
CalloutComponent,
DashboardComponent,
EnvironmentComponent,
FallbackSrcDirective,
@@ -68,6 +73,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
MoreComponent,
SearchCiphersPipe,
SettingsComponent,
SsoComponent,
StopClickDirective,
StopPropDirective,
TabsComponent,

View File

@@ -11,6 +11,7 @@ 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 { ElectronStorageService } from 'jslib/electron/services/electronStorage.service';
import { AuthGuardService } from './auth-guard.service';
import { LaunchGuardService } from './launch-guard.service';
@@ -31,8 +32,9 @@ import { ConstantsService } from 'jslib/services/constants.service';
import { ContainerService } from 'jslib/services/container.service';
import { CryptoService } from 'jslib/services/crypto.service';
import { EnvironmentService } from 'jslib/services/environment.service';
import { LowdbStorageService } from 'jslib/services/lowdbStorage.service';
import { NodeCryptoFunctionService } from 'jslib/services/nodeCryptoFunction.service';
import { PasswordGenerationService } from 'jslib/services/passwordGeneration.service';
import { PolicyService } from 'jslib/services/policy.service';
import { StateService } from 'jslib/services/state.service';
import { TokenService } from 'jslib/services/token.service';
import { UserService } from 'jslib/services/user.service';
@@ -46,7 +48,11 @@ import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib/abstr
import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service';
import { LogService as LogServiceAbstraction } from 'jslib/abstractions/log.service';
import { MessagingService as MessagingServiceAbstraction } from 'jslib/abstractions/messaging.service';
import {
PasswordGenerationService as PasswordGenerationServiceAbstraction,
} from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.service';
import { PolicyService as PolicyServiceAbstraction } from 'jslib/abstractions/policy.service';
import { StateService as StateServiceAbstraction } from 'jslib/abstractions/state.service';
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
import { TokenService as TokenServiceAbstraction } from 'jslib/abstractions/token.service';
@@ -57,11 +63,12 @@ const i18nService = new I18nService(window.navigator.language, './locales');
const stateService = new StateService();
const broadcasterService = new BroadcasterService();
const messagingService = new ElectronRendererMessagingService(broadcasterService);
const platformUtilsService = new ElectronPlatformUtilsService(i18nService, messagingService, true);
const storageService: StorageServiceAbstraction = new LowdbStorageService(null, remote.app.getPath('userData'));
const storageService: StorageServiceAbstraction = new ElectronStorageService(remote.app.getPath('userData'));
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);
const cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService,
platformUtilsService, logService);
const appIdService = new AppIdService(storageService);
const tokenService = new TokenService(storageService);
const apiService = new ApiService(tokenService, platformUtilsService,
@@ -70,10 +77,12 @@ const environmentService = new EnvironmentService(apiService, storageService, nu
const userService = new UserService(tokenService, storageService);
const containerService = new ContainerService(cryptoService);
const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
i18nService, platformUtilsService, messagingService, false);
i18nService, platformUtilsService, messagingService, null, logService, false);
const configurationService = new ConfigurationService(storageService, secureStorageService);
const syncService = new SyncService(configurationService, logService, cryptoFunctionService, apiService,
messagingService, i18nService);
const passwordGenerationService = new PasswordGenerationService(cryptoService, storageService, null);
const policyService = new PolicyService(userService, storageService);
const analytics = new Analytics(window, () => true, platformUtilsService, storageService, appIdService);
containerService.attachToWindow(window);
@@ -134,6 +143,9 @@ export function initFactory(): Function {
{ 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,

View File

@@ -14,7 +14,8 @@
<strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong>
<strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong>
</p>
<button #startBtn (click)="start()" [appApiAction]="startPromise" class="btn btn-primary" [disabled]="startBtn.loading">
<button #startBtn (click)="start()" [appApiAction]="startPromise" class="btn btn-primary"
[disabled]="startBtn.loading">
<i class="fa fa-play fa-fw" [hidden]="startBtn.loading"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startBtn.loading"></i>
{{'startSync' | i18n}}
@@ -23,7 +24,8 @@
<i class="fa fa-stop fa-fw"></i>
{{'stopSync' | i18n}}
</button>
<button #syncBtn (click)="sync()" [appApiAction]="syncPromise" class="btn btn-primary" [disabled]="syncBtn.loading">
<button #syncBtn (click)="sync()" [appApiAction]="syncPromise" class="btn btn-primary"
[disabled]="syncBtn.loading">
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncBtn.loading}"></i>
{{'syncNow' | i18n}}
</button>
@@ -33,7 +35,8 @@
<h3 class="card-header">{{'testing' | i18n}}</h3>
<div class="card-body">
<p>{{'testingDesc' | i18n}}</p>
<button #simBtn (click)="simulate()" [appApiAction]="simPromise" class="btn btn-primary" [disabled]="simBtn.loading">
<button #simBtn (click)="simulate()" [appApiAction]="simPromise" class="btn btn-primary"
[disabled]="simBtn.loading">
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simBtn.loading"></i>
<i class="fa fa-bug fa-fw" [hidden]="simBtn.loading"></i>
{{'testNow' | i18n}}

View File

@@ -12,18 +12,17 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { StateService } from 'jslib/abstractions/state.service';
import { AzureDirectoryService } from '../../services/azure-directory.service';
import { GSuiteDirectoryService } from '../../services/gsuite-directory.service';
import { LdapDirectoryService } from '../../services/ldap-directory.service';
import { SyncService } from '../../services/sync.service';
import { Entry } from '../../models/entry';
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';
@Component({
@@ -36,7 +35,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
simEnabledUsers: UserEntry[] = [];
simDisabledUsers: UserEntry[] = [];
simDeletedUsers: UserEntry[] = [];
simPromise: Promise<any>;
simPromise: Promise<SimResult>;
simSinceLast: boolean = false;
syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
startPromise: Promise<any>;
@@ -103,63 +102,18 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.simDisabledUsers = [];
this.simDeletedUsers = [];
this.simPromise = new Promise(async (resolve, reject) => {
try {
const result = await this.syncService.sync(!this.simSinceLast, true);
if (result[0] != null) {
this.simGroups = result[0];
}
if (result[1] != null) {
this.simUsers = result[1];
}
} catch (e) {
this.simGroups = null;
this.simUsers = null;
reject(e || this.i18nService.t('syncError'));
return;
}
const userMap = new Map<string, UserEntry>();
this.sort(this.simUsers);
for (const u of this.simUsers) {
userMap.set(u.externalId, u);
if (u.deleted) {
this.simDeletedUsers.push(u);
} else if (u.disabled) {
this.simDisabledUsers.push(u);
} else {
this.simEnabledUsers.push(u);
}
}
this.sort(this.simGroups);
for (const g of this.simGroups) {
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.sort(anyG.users);
}
resolve();
});
}
private sort(arr: Entry[]) {
arr.sort((a, b) => {
return this.i18nService.collator ? this.i18nService.collator.compare(a.displayName, b.displayName) :
a.displayName.localeCompare(b.displayName);
});
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() {

View File

@@ -6,7 +6,7 @@
<p>
{{'bitwardenDirectoryConnector' | i18n}}
<br /> {{'version' | i18n : version}}
<br /> &copy; 8bit Solutions LLC 2015-{{year}}
<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>

View File

@@ -12,7 +12,8 @@
<div [hidden]="directory != directoryType.Ldap">
<div class="form-group">
<label for="hostname">{{'serverHostname' | i18n}}</label>
<input type="text" class="form-control" id="hostname" name="Hostname" [(ngModel)]="ldap.hostname">
<input type="text" class="form-control" id="hostname" name="Hostname"
[(ngModel)]="ldap.hostname">
<small class="text-muted form-text">{{'ex' | i18n}} ad.company.com</small>
</div>
<div class="form-group">
@@ -22,7 +23,8 @@
</div>
<div class="form-group">
<label for="rootPath">{{'rootPath' | i18n}}</label>
<input type="text" class="form-control" id="rootPath" name="RootPath" [(ngModel)]="ldap.rootPath">
<input type="text" class="form-control" id="rootPath" name="RootPath"
[(ngModel)]="ldap.rootPath">
<small class="text-muted form-text">{{'ex' | i18n}} dc=company,dc=com</small>
</div>
<div class="form-group">
@@ -31,52 +33,97 @@
<label class="form-check-label" for="ad">{{'ldapAd' | i18n}}</label>
</div>
</div>
<div class="form-group" *ngIf="!ldap.ad">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="pagedSearch"
[(ngModel)]="ldap.pagedSearch" name="PagedSearch">
<label class="form-check-label"
for="pagedSearch">{{'ldapPagedResults' | i18n}}</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="ssl" [(ngModel)]="ldap.ssl" name="SSL">
<label class="form-check-label" for="ssl">{{'ldapSsl' | i18n}}</label>
<input class="form-check-input" type="checkbox" id="ldapEncrypted" [(ngModel)]="ldap.ssl"
name="Encrypted">
<label class="form-check-label" for="ldapEncrypted">{{'ldapEncrypted' | i18n}}</label>
</div>
</div>
<div class="ml-4" *ngIf="ldap.ssl">
<p>{{'ldapSslUntrustedDesc' | i18n}}</p>
<div class="form-group">
<label for="sslCertPath">{{'ldapSslCert' | i18n}}</label>
<input type="file" class="form-control-file mb-2" id="sslCertPath_file" (change)="setSslPath('sslCertPath')">
<input type="text" class="form-control" id="sslCertPath" name="SSLCertPath" [(ngModel)]="ldap.sslCertPath">
<div class="form-radio">
<input class="form-radio-input" type="radio" [value]="false" id="ssl"
[(ngModel)]="ldap.startTls" name="SSL">
<label class="form-radio-label" for="ssl">{{'ldapSsl' | i18n}}</label>
</div>
<div class="form-radio">
<input class="form-radio-input" type="radio" [value]="true" id="startTls"
[(ngModel)]="ldap.startTls" name="StartTLS">
<label class="form-radio-label" for="startTls">{{'ldapTls' | i18n}}</label>
</div>
</div>
<div class="form-group">
<label for="sslKeyPath">{{'ldapSslKey' | i18n}}</label>
<input type="file" class="form-control-file mb-2" id="sslKeyPath_file" (change)="setSslPath('sslKeyPath')">
<input type="text" class="form-control" id="sslKeyPath" name="SSLKeyPath" [(ngModel)]="ldap.sslKeyPath">
<div class="ml-4" *ngIf="ldap.startTls">
<p>{{'ldapTlsUntrustedDesc' | i18n}}</p>
<div class="form-group">
<label for="tlsCaPath">{{'ldapTlsCa' | i18n}}</label>
<input type="file" class="form-control-file mb-2" id="tlsCaPath_file"
(change)="setSslPath('tlsCaPath')">
<input type="text" class="form-control" id="tlsCaPath" name="TLSCaPath"
[(ngModel)]="ldap.tlsCaPath">
</div>
</div>
<div class="form-group">
<label for="sslCaPath">{{'ldapSslCa' | i18n}}</label>
<input type="file" class="form-control-file mb-2" id="sslCaPath_file" (change)="setSslPath('sslCaPath')">
<input type="text" class="form-control" id="sslCaPath" name="SSLCaPath" [(ngModel)]="ldap.sslCaPath">
<div class="ml-4" *ngIf="!ldap.startTls">
<p>{{'ldapSslUntrustedDesc' | i18n}}</p>
<div class="form-group">
<label for="sslCertPath">{{'ldapSslCert' | i18n}}</label>
<input type="file" class="form-control-file mb-2" id="sslCertPath_file"
(change)="setSslPath('sslCertPath')">
<input type="text" class="form-control" id="sslCertPath" name="SSLCertPath"
[(ngModel)]="ldap.sslCertPath">
</div>
<div class="form-group">
<label for="sslKeyPath">{{'ldapSslKey' | i18n}}</label>
<input type="file" class="form-control-file mb-2" id="sslKeyPath_file"
(change)="setSslPath('sslKeyPath')">
<input type="text" class="form-control" id="sslKeyPath" name="SSLKeyPath"
[(ngModel)]="ldap.sslKeyPath">
</div>
<div class="form-group">
<label for="sslCaPath">{{'ldapSslCa' | i18n}}</label>
<input type="file" class="form-control-file mb-2" id="sslCaPath_file"
(change)="setSslPath('sslCaPath')">
<input type="text" class="form-control" id="sslCaPath" name="SSLCaPath"
[(ngModel)]="ldap.sslCaPath">
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sslAllowUnauthorized" [(ngModel)]="ldap.sslAllowUnauthorized" name="SSLAllowUnauthorized">
<label class="form-check-label" for="sslAllowUnauthorized">{{'ldapSslAllowUnauthorized' | i18n}}</label>
<input class="form-check-input" type="checkbox" id="certDoNotVerify"
[(ngModel)]="ldap.sslAllowUnauthorized" name="CertDoNoVerify">
<label class="form-check-label"
for="certDoNotVerify">{{'ldapCertDoNotVerify' | i18n}}</label>
</div>
</div>
</div>
<div class="form-group" [hidden]="true">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="currentUser" [(ngModel)]="ldap.currentUser" name="CurrentUser">
<input class="form-check-input" type="checkbox" id="currentUser"
[(ngModel)]="ldap.currentUser" name="CurrentUser">
<label class="form-check-label" for="currentUser">{{'currentUser' | i18n}}</label>
</div>
</div>
<div [hidden]="ldap.currentUser">
<div class="form-group">
<label for="username">{{'username' | i18n}}</label>
<input type="text" class="form-control" id="username" name="Username" [(ngModel)]="ldap.username">
<input type="text" class="form-control" id="username" name="Username"
[(ngModel)]="ldap.username">
<small class="text-muted form-text" *ngIf="ldap.ad">{{'ex' | i18n}} company\admin</small>
<small class="text-muted form-text" *ngIf="!ldap.ad">{{'ex' | i18n}} cn=admin,dc=company,dc=com</small>
<small class="text-muted form-text" *ngIf="!ldap.ad">{{'ex' | i18n}}
cn=admin,dc=company,dc=com</small>
</div>
<div class="form-group">
<label for="password">{{'password' | i18n}}</label>
<input type="password" class="form-control" id="password" name="Password" [(ngModel)]="ldap.password">
<input type="password" class="form-control" id="password" name="Password"
[(ngModel)]="ldap.password">
</div>
</div>
</div>
@@ -88,7 +135,8 @@
</div>
<div class="form-group">
<label for="applicationId">{{'applicationId' | i18n}}</label>
<input type="text" class="form-control" id="applicationId" name="ApplicationId" [(ngModel)]="azure.applicationId">
<input type="text" class="form-control" id="applicationId" name="ApplicationId"
[(ngModel)]="azure.applicationId">
</div>
<div class="form-group">
<label for="secretKey">{{'secretKey' | i18n}}</label>
@@ -103,7 +151,28 @@
</div>
<div class="form-group">
<label for="oktaToken">{{'token' | i18n}}</label>
<input type="text" class="form-control" id="oktaToken" name="OktaToken" [(ngModel)]="okta.token">
<input type="text" class="form-control" id="oktaToken" name="OktaToken"
[(ngModel)]="okta.token">
</div>
</div>
<div [hidden]="directory != directoryType.OneLogin">
<div class="form-group">
<label for="oneLoginClientId">{{'clientId' | i18n}}</label>
<input type="text" class="form-control" id="oneLoginClientId" name="OneLoginClientId"
[(ngModel)]="oneLogin.clientId">
</div>
<div class="form-group">
<label for="oneLoginClientSecret">{{'clientSecret' | i18n}}</label>
<input type="text" class="form-control" id="oneLoginClientSecret" name="OneLoginClientSecret"
[(ngModel)]="oneLogin.clientSecret">
</div>
<div class="form-group">
<label for="oneLoginRegion">{{'region' | i18n}}</label>
<select class="form-control" id="oneLoginRegion" name="OneLoginRegion"
[(ngModel)]="oneLogin.region">
<option value="us">US</option>
<option value="eu">EU</option>
</select>
</div>
</div>
<div [hidden]="directory != directoryType.GSuite">
@@ -114,26 +183,31 @@
</div>
<div class="form-group">
<label for="adminUser">{{'adminUser' | i18n}}</label>
<input type="text" class="form-control" id="adminUser" name="AdminUser" [(ngModel)]="gsuite.adminUser">
<input type="text" class="form-control" id="adminUser" name="AdminUser"
[(ngModel)]="gsuite.adminUser">
<small class="text-muted form-text">{{'ex' | i18n}} admin@company.com</small>
</div>
<div class="form-group">
<label for="customerId">{{'customerId' | i18n}}</label>
<input type="text" class="form-control" id="customerId" name="CustomerId" [(ngModel)]="gsuite.customer">
<input type="text" class="form-control" id="customerId" name="CustomerId"
[(ngModel)]="gsuite.customer">
<small class="text-muted form-text">{{'ex' | i18n}} 39204722352</small>
</div>
<div class="form-group">
<label for="keyFile">{{'jsonKeyFile' | i18n}}</label>
<input type="file" class="form-control-file" id="keyFile" accept=".json" (change)="parseKeyFile()">
<input type="file" class="form-control-file" id="keyFile" accept=".json"
(change)="parseKeyFile()">
<small class="text-muted form-text">{{'ex' | i18n}} My Project-jksd3jd223.json</small>
</div>
<div class="form-group" [hidden]="!gsuite.clientEmail">
<label for="clientEmail">{{'clientEmail' | i18n}}</label>
<input type="text" class="form-control" id="clientEmail" name="ClientEmail" [(ngModel)]="gsuite.clientEmail">
<input type="text" class="form-control" id="clientEmail" name="ClientEmail"
[(ngModel)]="gsuite.clientEmail">
</div>
<div class="form-group" [hidden]="!gsuite.privateKey">
<label for="privateKey">{{'privateKey' | i18n}}</label>
<textarea class="form-control text-monospace" id="privateKey" name="PrivateKey" [(ngModel)]="gsuite.privateKey">
<textarea class="form-control text-monospace" id="privateKey" name="PrivateKey"
[(ngModel)]="gsuite.privateKey">
</textarea>
</div>
</div>
@@ -158,49 +232,66 @@
<div class="card-body">
<div class="form-group">
<label for="interval">{{'interval' | i18n}}</label>
<input type="number" min="5" class="form-control" id="interval" name="Interval" [(ngModel)]="sync.interval">
<input type="number" min="5" class="form-control" id="interval" name="Interval"
[(ngModel)]="sync.interval">
<small class="text-muted form-text">{{'intervalMin' | i18n}}</small>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="removeDisabled" [(ngModel)]="sync.removeDisabled" name="RemoveDisabled">
<input class="form-check-input" type="checkbox" id="removeDisabled"
[(ngModel)]="sync.removeDisabled" name="RemoveDisabled">
<label class="form-check-label" for="removeDisabled">{{'removeDisabled' | i18n}}</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="overwriteExisting"
[(ngModel)]="sync.overwriteExisting" name="OverwriteExisting">
<label class="form-check-label" for="overwriteExisting">{{'overwriteExisting' | i18n}}</label>
</div>
</div>
<div [hidden]="directory != directoryType.Ldap">
<div [hidden]="ldap.ad">
<div class="form-group">
<label for="memberAttribute">{{'memberAttribute' | i18n}}</label>
<input type="text" class="form-control" id="memberAttribute" name="MemberAttribute" [(ngModel)]="sync.memberAttribute">
<input type="text" class="form-control" id="memberAttribute" name="MemberAttribute"
[(ngModel)]="sync.memberAttribute">
<small class="text-muted form-text">{{'ex' | i18n}} uniqueMember</small>
</div>
<div class="form-group">
<label for="creationDateAttribute">{{'creationDateAttribute' | i18n}}</label>
<input type="text" class="form-control" id="creationDateAttribute" name="CreationDateAttribute" [(ngModel)]="sync.creationDateAttribute">
<input type="text" class="form-control" id="creationDateAttribute"
name="CreationDateAttribute" [(ngModel)]="sync.creationDateAttribute">
<small class="text-muted form-text">{{'ex' | i18n}} whenCreated</small>
</div>
<div class="form-group">
<label for="revisionDateAttribute">{{'revisionDateAttribute' | i18n}}</label>
<input type="text" class="form-control" id="revisionDateAttribute" name="RevisionDateAttribute" [(ngModel)]="sync.revisionDateAttribute">
<input type="text" class="form-control" id="revisionDateAttribute"
name="RevisionDateAttribute" [(ngModel)]="sync.revisionDateAttribute">
<small class="text-muted form-text">{{'ex' | i18n}} whenChanged</small>
</div>
</div>
</div>
<div [hidden]="directory != directoryType.Ldap && directory != directoryType.OneLogin">
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="useEmailPrefixSuffix" [(ngModel)]="sync.useEmailPrefixSuffix" name="UseEmailPrefixSuffix">
<label class="form-check-label" for="useEmailPrefixSuffix">{{'useEmailPrefixSuffix' | i18n}}</label>
<input class="form-check-input" type="checkbox" id="useEmailPrefixSuffix"
[(ngModel)]="sync.useEmailPrefixSuffix" name="UseEmailPrefixSuffix">
<label class="form-check-label"
for="useEmailPrefixSuffix">{{'useEmailPrefixSuffix' | i18n}}</label>
</div>
</div>
<div [hidden]="!sync.useEmailPrefixSuffix">
<div class="form-group" [hidden]="ldap.ad">
<div class="form-group" [hidden]="ldap.ad || directory != directoryType.Ldap">
<label for="emailPrefixAttribute">{{'emailPrefixAttribute' | i18n}}</label>
<input type="text" class="form-control" id="emailPrefixAttribute" name="EmailPrefixAttribute" [(ngModel)]="sync.emailPrefixAttribute">
<input type="text" class="form-control" id="emailPrefixAttribute"
name="EmailPrefixAttribute" [(ngModel)]="sync.emailPrefixAttribute">
<small class="text-muted form-text">{{'ex' | i18n}} accountName</small>
</div>
<div class="form-group">
<label for="emailSuffix">{{'emailSuffix' | i18n}}</label>
<input type="text" class="form-control" id="emailSuffix" name="EmailSuffix" [(ngModel)]="sync.emailSuffix">
<input type="text" class="form-control" id="emailSuffix" name="EmailSuffix"
[(ngModel)]="sync.emailSuffix">
<small class="text-muted form-text">{{'ex' | i18n}} @company.com</small>
</div>
</div>
@@ -208,33 +299,43 @@
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="syncUsers" [(ngModel)]="sync.users" name="SyncUsers">
<input class="form-check-input" type="checkbox" id="syncUsers" [(ngModel)]="sync.users"
name="SyncUsers">
<label class="form-check-label" for="syncUsers">{{'syncUsers' | i18n}}</label>
</div>
</div>
<div [hidden]="!sync.users">
<div class="form-group">
<label for="userFilter">{{'userFilter' | i18n}}</label>
<textarea class="form-control" id="userFilter" name="UserFilter" [(ngModel)]="sync.userFilter"></textarea>
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.AzureActiveDirectory">{{'ex' | i18n}} exclude:joe@company.com</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}} exclude:joe@company.com | profile.firstName eq "John"</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}} exclude:joe@company.com | orgName=Engineering</small>
<textarea class="form-control" id="userFilter" name="UserFilter"
[(ngModel)]="sync.userFilter"></textarea>
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}}
(&amp;(givenName=John)(|(l=Dallas)(l=Austin)))</small>
<small class="text-muted form-text"
*ngIf="directory === directoryType.AzureActiveDirectory">{{'ex' | i18n}}
exclude:joe@company.com</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}}
exclude:joe@company.com | profile.firstName eq "John"</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}}
exclude:joe@company.com | orgName=Engineering</small>
</div>
<div class="form-group" [hidden]="directory != directoryType.Ldap">
<label for="userPath">{{'userPath' | i18n}}</label>
<input type="text" class="form-control" id="userPath" name="UserPath" [(ngModel)]="sync.userPath">
<input type="text" class="form-control" id="userPath" name="UserPath"
[(ngModel)]="sync.userPath">
<small class="text-muted form-text">{{'ex' | i18n}} CN=Users</small>
</div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="form-group">
<label for="userObjectClass">{{'userObjectClass' | i18n}}</label>
<input type="text" class="form-control" id="userObjectClass" name="UserObjectClass" [(ngModel)]="sync.userObjectClass">
<input type="text" class="form-control" id="userObjectClass" name="UserObjectClass"
[(ngModel)]="sync.userObjectClass">
<small class="text-muted form-text">{{'ex' | i18n}} inetOrgPerson</small>
</div>
<div class="form-group">
<label for="userEmailAttribute">{{'userEmailAttribute' | i18n}}</label>
<input type="text" class="form-control" id="userEmailAttribute" name="UserEmailAttribute" [(ngModel)]="sync.userEmailAttribute">
<input type="text" class="form-control" id="userEmailAttribute" name="UserEmailAttribute"
[(ngModel)]="sync.userEmailAttribute">
<small class="text-muted form-text">{{'ex' | i18n}} mail</small>
</div>
</div>
@@ -242,34 +343,44 @@
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="syncGroups" [(ngModel)]="sync.groups" name="SyncGroups">
<input class="form-check-input" type="checkbox" id="syncGroups" [(ngModel)]="sync.groups"
name="SyncGroups">
<label class="form-check-label" for="syncGroups">{{'syncGroups' | i18n}}</label>
</div>
</div>
<div [hidden]="!sync.groups">
<div class="form-group">
<label for="groupFilter">{{'groupFilter' | i18n}}</label>
<textarea class="form-control" id="groupFilter" name="GroupFilter" [(ngModel)]="sync.groupFilter"></textarea>
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}} (&amp;!(name=Sales*)!(name=IT*))</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.AzureActiveDirectory">{{'ex' | i18n}} include:Sales,IT</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}} include:Sales,IT | type eq "APP_GROUP"</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}} include:Sales,IT</small>
<textarea class="form-control" id="groupFilter" name="GroupFilter"
[(ngModel)]="sync.groupFilter"></textarea>
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}}
(&amp;!(name=Sales*)!(name=IT*))</small>
<small class="text-muted form-text"
*ngIf="directory === directoryType.AzureActiveDirectory">{{'ex' | i18n}}
include:Sales,IT</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}}
include:Sales,IT | type eq "APP_GROUP"</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}}
include:Sales,IT</small>
</div>
<div class="form-group" [hidden]="directory != directoryType.Ldap">
<label for="groupPath">{{'groupPath' | i18n}}</label>
<input type="text" class="form-control" id="groupPath" name="GroupPath" [(ngModel)]="sync.groupPath">
<input type="text" class="form-control" id="groupPath" name="GroupPath"
[(ngModel)]="sync.groupPath">
<small class="text-muted form-text" *ngIf="!ldap.ad">{{'ex' | i18n}} CN=Groups</small>
<small class="text-muted form-text" *ngIf="ldap.ad">{{'ex' | i18n}} CN=Users</small>
</div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="form-group">
<label for="groupObjectClass">{{'groupObjectClass' | i18n}}</label>
<input type="text" class="form-control" id="groupObjectClass" name="GroupObjectClass" [(ngModel)]="sync.groupObjectClass">
<input type="text" class="form-control" id="groupObjectClass" name="GroupObjectClass"
[(ngModel)]="sync.groupObjectClass">
<small class="text-muted form-text">{{'ex' | i18n}} groupOfUniqueNames</small>
</div>
<div class="form-group">
<label for="groupNameAttribute">{{'groupNameAttribute' | i18n}}</label>
<input type="text" class="form-control" id="groupNameAttribute" name="GroupNameAttribute" [(ngModel)]="sync.groupNameAttribute">
<input type="text" class="form-control" id="groupNameAttribute" name="GroupNameAttribute"
[(ngModel)]="sync.groupNameAttribute">
<small class="text-muted form-text">{{'ex' | i18n}} name</small>
</div>
</div>

View File

@@ -19,8 +19,11 @@ 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',
@@ -32,6 +35,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
gsuite = new GSuiteConfiguration();
azure = new AzureConfiguration();
okta = new OktaConfiguration();
oneLogin = new OneLoginConfiguration();
sync = new SyncConfiguration();
organizationId: string;
directoryOptions: any[];
@@ -46,6 +50,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
{ name: 'Azure Active Directory', value: DirectoryType.AzureActiveDirectory },
{ name: 'G Suite (Google)', value: DirectoryType.GSuite },
{ name: 'Okta', value: DirectoryType.Okta },
{ name: 'OneLogin', value: DirectoryType.OneLogin },
];
}
@@ -68,6 +73,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
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;
}
@@ -76,38 +83,17 @@ export class SettingsComponent implements OnInit, OnDestroy {
}
async submit() {
if (this.ldap.ad) {
this.sync.creationDateAttribute = 'whenCreated';
this.sync.revisionDateAttribute = 'whenChanged';
this.sync.emailPrefixAttribute = 'sAMAccountName';
this.sync.memberAttribute = 'member';
this.sync.userObjectClass = 'person';
this.sync.groupObjectClass = 'group';
this.sync.userEmailAttribute = 'mail';
this.sync.groupNameAttribute = 'name';
if (this.sync.groupPath == null) {
this.sync.groupPath = 'CN=Users';
}
if (this.sync.userPath == null) {
this.sync.userPath = 'CN=Users';
}
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
if (this.ldap != null && this.ldap.ad) {
this.ldap.pagedSearch = true;
}
if (this.sync.interval != null) {
if (this.sync.interval <= 0) {
this.sync.interval = null;
} else if (this.sync.interval < 5) {
this.sync.interval = 5;
}
}
await this.configurationService.saveOrganizationId(this.organizationId);
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);
}
@@ -119,7 +105,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
const reader = new FileReader();
reader.readAsText(filePicker.files[0], 'utf-8');
reader.onload = (evt) => {
reader.onload = evt => {
this.ngZone.run(async () => {
try {
const result = JSON.parse((evt.target as FileReader).result as string);

142
src/bwdc.ts Normal file
View File

@@ -0,0 +1,142 @@
import * as fs from 'fs';
import * as path from 'path';
import { LogLevelType } from 'jslib/enums/logLevelType';
import { AuthService } from 'jslib/services/auth.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 { SyncService } from './services/sync.service';
import { CliPlatformUtilsService } from 'jslib/cli/services/cliPlatformUtils.service';
import { ConsoleLogService } from 'jslib/cli/services/consoleLog.service';
import { AppIdService } from 'jslib/services/appId.service';
import { ConstantsService } from 'jslib/services/constants.service';
import { ContainerService } from 'jslib/services/container.service';
import { CryptoService } from 'jslib/services/crypto.service';
import { EnvironmentService } from 'jslib/services/environment.service';
import { NodeApiService } from 'jslib/services/nodeApi.service';
import { NodeCryptoFunctionService } from 'jslib/services/nodeCryptoFunction.service';
import { NoopMessagingService } from 'jslib/services/noopMessaging.service';
import { PasswordGenerationService } from 'jslib/services/passwordGeneration.service';
import { TokenService } from 'jslib/services/token.service';
import { UserService } from 'jslib/services/user.service';
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
import { Program } from './program';
// tslint:disable-next-line
const packageJson = require('./package.json');
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;
userService: UserService;
containerService: ContainerService;
cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService;
configurationService: ConfigurationService;
syncService: SyncService;
passwordGenerationService: PasswordGenerationService;
program: Program;
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.apiService = new NodeApiService(this.tokenService, this.platformUtilsService,
async (expired: boolean) => await this.logout());
this.environmentService = new EnvironmentService(this.apiService, this.storageService, null);
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, 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.passwordGenerationService = new PasswordGenerationService(this.cryptoService, this.storageService, null);
this.program = new Program(this);
}
async run() {
await this.init();
this.program.run();
}
async logout() {
await Promise.all([
this.tokenService.clearToken(),
this.userService.clear(),
]);
}
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();
const installedVersion = await this.storageService.get<string>(ConstantsService.installedVersionKey);
const currentVersion = this.platformUtilsService.getApplicationVersion();
if (installedVersion == null || installedVersion !== currentVersion) {
await this.storageService.save(ConstantsService.installedVersionKey, currentVersion);
}
}
}
const main = new Main();
main.run();

View File

@@ -0,0 +1,22 @@
import * as program from 'commander';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { ConfigurationService } from '../services/configuration.service';
import { Response } from 'jslib/cli/models/response';
import { MessageResponse } from 'jslib/cli/models/response/messageResponse';
export class ClearCacheCommand {
constructor(private configurationService: ConfigurationService, private i18nService: I18nService) { }
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);
}
}
}

View File

@@ -0,0 +1,150 @@
import * as program from 'commander';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { ConfigurationService } from '../services/configuration.service';
import { DirectoryType } from '../enums/directoryType';
import { Response } from 'jslib/cli/models/response';
import { MessageResponse } from 'jslib/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/misc/nodeUtils';
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();
constructor(private environmentService: EnvironmentService, private i18nService: I18nService,
private configurationService: ConfigurationService) { }
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);
}
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 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 setLdapPassword(password: string) {
await this.loadConfig();
this.ldap.password = password;
await this.saveConfig();
}
private async setGSuiteKey(key: string) {
await this.loadConfig();
this.gsuite.privateKey = key != null ? key.trimLeft() : null;
await this.saveConfig();
}
private async setAzureKey(key: string) {
await this.loadConfig();
this.azure.key = key;
await this.saveConfig();
}
private async setOktaToken(token: string) {
await this.loadConfig();
this.okta.token = token;
await this.saveConfig();
}
private async setOneLoginSecret(secret: string) {
await this.loadConfig();
this.oneLogin.clientSecret = secret;
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 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);
}
}

View File

@@ -0,0 +1,29 @@
import * as program from 'commander';
import { ConfigurationService } from '../services/configuration.service';
import { Response } from 'jslib/cli/models/response';
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
export class LastSyncCommand {
constructor(private configurationService: ConfigurationService) { }
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);
}
}
}

View File

@@ -0,0 +1,25 @@
import * as program from 'commander';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { SyncService } from '../services/sync.service';
import { Response } from 'jslib/cli/models/response';
import { MessageResponse } from 'jslib/cli/models/response/messageResponse';
export class SyncCommand {
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);
}
}
}

View File

@@ -0,0 +1,24 @@
import * as program from 'commander';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { SyncService } from '../services/sync.service';
import { ConnectorUtils } from '../utils';
import { Response } from 'jslib/cli/models/response';
import { TestResponse } from '../models/response/testResponse';
export class TestCommand {
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);
}
}
}

View File

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

1
src/global.d.ts vendored
View File

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

View File

@@ -129,7 +129,7 @@
"message": "Self-hosted Environment"
},
"selfHostedEnvironmentFooter": {
"message": "Specify the base URL of your on-premise hosted Bitwarden installation."
"message": "Specify the base URL of your on-premises hosted Bitwarden installation."
},
"customEnvironment": {
"message": "Custom Environment"
@@ -140,6 +140,9 @@
"baseUrl": {
"message": "Server URL"
},
"webVaultUrl": {
"message": "Web Vault Server URL"
},
"apiUrl": {
"message": "API Server URL"
},
@@ -414,8 +417,20 @@
"sync": {
"message": "Sync"
},
"ldapEncrypted": {
"message": "This server uses an encrypted connection"
},
"ldapTls": {
"message": "Use TLS (STARTTLS)"
},
"ldapTlsCa": {
"message": "Certificate CA Chain (PEM)"
},
"ldapSsl": {
"message": "This server uses SSL (LDAPS)"
"message": "Use SSL (LDAPS)"
},
"ldapTlsUntrustedDesc": {
"message": "If your LDAP server uses a self-signed certificate for STARTTLS, you can configure certificate options below."
},
"ldapSslUntrustedDesc": {
"message": "If your LDAPS server uses an untrusted certificate you can configure certificate options below."
@@ -429,12 +444,15 @@
"ldapSslKey": {
"message": "Certificate Private Key (PEM)"
},
"ldapSslAllowUnauthorized": {
"message": "Allow untrusted SSL connections (not recommended)."
"ldapCertDoNotVerify": {
"message": "Do not verify server certificates (not recommended)."
},
"ldapAd": {
"message": "This server uses Active Directory"
},
"ldapPagedResults": {
"message": "This server pages search results"
},
"select": {
"message": "Select"
},
@@ -516,6 +534,9 @@
"noGroups": {
"message": "No groups to list."
},
"syncingComplete": {
"message": "Syncing complete."
},
"syncingStarted": {
"message": "Syncing started."
},
@@ -565,5 +586,141 @@
},
"hideToTray": {
"message": "Hide to Tray"
},
"alwaysOnTop": {
"message": "Always on Top",
"description": "Application window should always stay on top of other windows"
},
"hideToMenuBar": {
"message": "Hide to Menu Bar"
},
"savedSetting": {
"message": "Saved setting `$SETTING_NAME$`.",
"placeholders": {
"setting_name": {
"content": "$1",
"example": "server"
}
}
},
"overwriteExisting": {
"message": "Overwrite existing organization users based on current sync settings."
},
"clientId": {
"message": "Client ID"
},
"clientSecret": {
"message": "Client Secret"
},
"region": {
"message": "Region"
},
"enterpriseSingleSignOn": {
"message": "Enterprise Single Sign-On"
},
"setMasterPassword": {
"message": "Set Master Password"
},
"ssoCompleteRegistration": {
"message": "In order to complete logging in with SSO, please set a master password to access and protect your vault."
},
"newMasterPass": {
"message": "New Master Password"
},
"confirmNewMasterPass": {
"message": "Confirm New Master Password"
},
"masterPasswordPolicyInEffect": {
"message": "One or more organization policies require your master password to meet the following requirements:"
},
"policyInEffectMinComplexity": {
"message": "Minimum complexity score of $SCORE$",
"placeholders": {
"score": {
"content": "$1",
"example": "4"
}
}
},
"policyInEffectMinLength": {
"message": "Minimum length of $LENGTH$",
"placeholders": {
"length": {
"content": "$1",
"example": "14"
}
}
},
"policyInEffectUppercase": {
"message": "Contain one or more uppercase characters"
},
"policyInEffectLowercase": {
"message": "Contain one or more lowercase characters"
},
"policyInEffectNumbers": {
"message": "Contain one or more numbers"
},
"policyInEffectSpecial": {
"message": "Contain one or more of the following special characters $CHARS$",
"placeholders": {
"chars": {
"content": "$1",
"example": "!@#$%^&*"
}
}
},
"masterPassDesc": {
"message": "The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it."
},
"reTypeMasterPass": {
"message": "Re-type Master Password"
},
"masterPassHint": {
"message": "Master Password Hint (optional)"
},
"masterPassHintDesc": {
"message": "A master password hint can help you remember your password if you forget it."
},
"strong": {
"message": "Strong",
"description": "ex. A strong password. Scale: Weak -> Good -> Strong"
},
"good": {
"message": "Good",
"description": "ex. A good password. Scale: Weak -> Good -> Strong"
},
"weak": {
"message": "Weak",
"description": "ex. A weak password. Scale: Weak -> Good -> Strong"
},
"weakMasterPassword": {
"message": "Weak Master Password"
},
"weakMasterPasswordDesc": {
"message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?"
},
"errorOccurred": {
"message": "An error has occurred."
},
"error": {
"message": "Error"
},
"masterPassLength": {
"message": "Master password must be at least 8 characters long."
},
"masterPassDoesntMatch": {
"message": "Master password confirmation does not match."
},
"masterPasswordPolicyRequirementsNotMet": {
"message": "Your new master password does not meet the policy requirements."
},
"loading": {
"message": "Loading"
},
"setMasterPasswordRedirect": {
"message": "In order to log in with SSO from the Directory Connector, you must first log in through the web vault to set your master password."
},
"launchWebVault": {
"message": "Launch Web Vault"
}
}

View File

@@ -5,11 +5,10 @@ import { MenuMain } from './main/menu.main';
import { MessagingMain } from './main/messaging.main';
import { I18nService } from './services/i18n.service';
import { LowdbStorageService } from 'jslib/services/lowdbStorage.service';
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';
@@ -17,7 +16,7 @@ import { WindowMain } from 'jslib/electron/window.main';
export class Main {
logService: ElectronLogService;
i18nService: I18nService;
storageService: LowdbStorageService;
storageService: ElectronStorageService;
messagingService: ElectronMainMessagingService;
keytarStorageListener: KeytarStorageListener;
@@ -42,7 +41,7 @@ export class Main {
app.setPath('logs', path.join(app.getPath('userData'), 'logs'));
const args = process.argv.slice(1);
const watch = args.some((val) => val === '--watch');
const watch = args.some(val => val === '--watch');
if (watch) {
// tslint:disable-next-line
@@ -51,9 +50,9 @@ export class Main {
this.logService = new ElectronLogService(null, app.getPath('userData'));
this.i18nService = new I18nService('en', './locales/');
this.storageService = new LowdbStorageService(null, app.getPath('userData'));
this.storageService = new ElectronStorageService(app.getPath('userData'));
this.windowMain = new WindowMain(this.storageService, 800, 600);
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');
@@ -61,10 +60,10 @@ export class Main {
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.messagingService = new ElectronMainMessagingService(this.windowMain, message => {
this.messagingMain.onMessage(message);
});
@@ -72,7 +71,6 @@ export class Main {
}
bootstrap() {
this.storageService.init();
this.keytarStorageListener.init();
this.windowMain.init().then(async () => {
await this.i18nService.init(app.getLocale());
@@ -80,11 +78,32 @@ export class Main {
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) => {
// tslint: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

@@ -48,11 +48,19 @@ export class MenuMain extends BaseMenu {
template[template.length - 1].submenu = this.macWindowSubmenuOptions;
}
(template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(1, 0, {
label: this.main.i18nService.t('hideToTray'),
click: () => this.main.messagingService.send('hideToTray'),
accelerator: 'CmdOrCtrl+Shift+M',
});
(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);

View File

@@ -1,5 +1,7 @@
export class LdapConfiguration {
ssl = false;
startTls = false;
tlsCaPath: string;
sslAllowUnauthorized = false;
sslCertPath: string;
sslKeyPath: string;
@@ -12,4 +14,5 @@ export class LdapConfiguration {
username: string;
password: string;
ad = true;
pagedSearch = true;
}

View File

@@ -0,0 +1,5 @@
export class OneLoginConfiguration {
clientId: string;
clientSecret: string;
region = 'us';
}

View File

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

View File

@@ -0,0 +1,22 @@
import { GroupResponse } from './groupResponse';
import { UserResponse } from './userResponse';
import { SimResult } from '../simResult';
import { BaseResponse } from 'jslib/cli/models/response/baseResponse';
export class TestResponse implements BaseResponse {
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)) : [];
}
}

View File

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

10
src/models/simResult.ts Normal file
View File

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

View File

@@ -5,6 +5,7 @@ export class SyncConfiguration {
userFilter: string;
groupFilter: string;
removeDisabled = false;
overwriteExisting = false;
// Ldap properties
groupObjectClass: string;
userObjectClass: string;

303
src/package-lock.json generated
View File

@@ -1,303 +0,0 @@
{
"name": "bitwarden",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "1.0.3"
}
},
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
},
"bluebird-lst": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz",
"integrity": "sha512-Ey0bDNys5qpYPhZ/oQ9vOEvD0TYQDTILMXWP2iGfvMg7rSDde+oV4aQQgqRH+CvBFNz2BSDQnPGMUl6LKBUUQA==",
"requires": {
"bluebird": "3.5.1"
}
},
"builder-util-runtime": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.2.0.tgz",
"integrity": "sha512-cROCExnJOJvRD58HHcnrrgyRAoDHGZT0hKox0op7vTuuuRC/1JKMXvSR+Hxy7KWy/aEmKu0HfSqMd4znDEqQsA==",
"requires": {
"bluebird-lst": "1.0.5",
"debug": "3.1.0",
"fs-extra-p": "4.5.2",
"sax": "1.2.4"
}
},
"conf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz",
"integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==",
"requires": {
"dot-prop": "4.2.0",
"env-paths": "1.0.0",
"make-dir": "1.2.0",
"pkg-up": "2.0.0",
"write-file-atomic": "2.3.0"
}
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"dot-prop": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
"integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
"requires": {
"is-obj": "1.0.1"
}
},
"electron-is-dev": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.3.0.tgz",
"integrity": "sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4="
},
"electron-log": {
"version": "2.2.14",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.14.tgz",
"integrity": "sha512-Rj+XyK4nShe/nv9v1Uks4KEfjtQ6N+eSnx5CLpAjG6rlyUdAflyFHoybcHSLoq9l9pGavclULWS5IXgk8umc2g=="
},
"electron-store": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/electron-store/-/electron-store-1.3.0.tgz",
"integrity": "sha512-r1Pdl5MwpiCxgbsl0qnwv/GABO5+J/JTO16+KyqL+bOITIk9o3cq3Sw69uO9NgPkpfcKeEwxtJFbtbiBlGTiDA==",
"requires": {
"conf": "1.4.0"
}
},
"electron-updater": {
"version": "2.21.4",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-2.21.4.tgz",
"integrity": "sha512-x6QSbyxgGR3szIOBtFoCJH0TfgB55AWHaXmilNgorfvpnCdEMQEATxEzLOW0JCzzcB5y3vBrawvmMUEdXwutmA==",
"requires": {
"bluebird-lst": "1.0.5",
"builder-util-runtime": "4.2.0",
"electron-is-dev": "0.3.0",
"fs-extra-p": "4.5.2",
"js-yaml": "3.11.0",
"lazy-val": "1.0.3",
"lodash.isequal": "4.5.0",
"semver": "5.5.0",
"source-map-support": "0.5.4"
}
},
"env-paths": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz",
"integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA="
},
"esprima": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
"integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw=="
},
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"requires": {
"locate-path": "2.0.0"
}
},
"fs-extra": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz",
"integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==",
"requires": {
"graceful-fs": "4.1.11",
"jsonfile": "4.0.0",
"universalify": "0.1.1"
}
},
"fs-extra-p": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.5.2.tgz",
"integrity": "sha512-ZYqFpBdy9w7PsK+vB30j+TnHOyWHm/CJbUq1qqoE8tb71m6qgk5Wa7gp3MYQdlGFxb9vfznF+yD4jcl8l+y91A==",
"requires": {
"bluebird-lst": "1.0.5",
"fs-extra": "5.0.0"
}
},
"graceful-fs": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
},
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
},
"is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
},
"js-yaml": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz",
"integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==",
"requires": {
"argparse": "1.0.10",
"esprima": "4.0.0"
}
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"requires": {
"graceful-fs": "4.1.11"
}
},
"keytar": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/keytar/-/keytar-4.1.0.tgz",
"integrity": "sha512-L3KqiSMtpVitlug4uuI+K5XLne9SAVEFWE8SCQIhQiH0IA/CTbon5v5prVLKK0Ken54o2O8V9HceKagpwJum+Q==",
"requires": {
"nan": "2.5.1"
}
},
"lazy-val": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz",
"integrity": "sha512-pjCf3BYk+uv3ZcPzEVM0BFvO9Uw58TmlrU0oG5tTrr9Kcid3+kdKxapH8CjdYmVa2nO5wOoZn2rdvZx2PKj/xg=="
},
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
"requires": {
"p-locate": "2.0.0",
"path-exists": "3.0.0"
}
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"make-dir": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz",
"integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==",
"requires": {
"pify": "3.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"nan": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz",
"integrity": "sha1-1bAWkSUzJql6K77p5hxV2NYDUeI="
},
"p-limit": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz",
"integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==",
"requires": {
"p-try": "1.0.0"
}
},
"p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"requires": {
"p-limit": "1.2.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"pkg-up": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
"requires": {
"find-up": "2.1.0"
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
},
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"source-map-support": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.4.tgz",
"integrity": "sha512-PETSPG6BjY1AHs2t64vS2aqAgu6dMIMXJULWFBGbh2Gr8nVLbCFDo6i/RMMvviIQ2h1Z8+5gQhVKSn2je9nmdg==",
"requires": {
"source-map": "0.6.1"
}
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"universalify": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
},
"write-file-atomic": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz",
"integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==",
"requires": {
"graceful-fs": "4.1.11",
"imurmurhash": "0.1.4",
"signal-exit": "3.0.2"
}
}
}
}

View File

@@ -2,19 +2,20 @@
"name": "bitwarden-directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2.4.0",
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
"version": "2.9.1",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
"main": "main.js",
"repository": {
"type": "git",
"url": "https://github.com/bitwarden/desktop"
"url": "https://github.com/bitwarden/directory-connector"
},
"dependencies": {
"electron-log": "2.2.14",
"electron-updater": "3.0.3",
"keytar": "4.2.1",
"lowdb": "1.0.0"
"browser-hrtime": "^1.1.8",
"electron-log": "4.3.0",
"electron-store": "6.0.1",
"electron-updater": "4.3.5",
"keytar": "7.3.0"
}
}

283
src/program.ts Normal file
View File

@@ -0,0 +1,283 @@
import * as chk from 'chalk';
import * as program from 'commander';
import * as path from 'path';
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 { LoginCommand } from 'jslib/cli/commands/login.command';
import { LogoutCommand } from 'jslib/cli/commands/logout.command';
import { UpdateCommand } from 'jslib/cli/commands/update.command';
import { BaseProgram } from 'jslib/cli/baseProgram';
import { Response } from 'jslib/cli/models/response';
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
const chalk = chk.default;
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');
}
};
export class Program extends BaseProgram {
constructor(private main: Main) {
super(main.userService, writeLn);
}
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('--quiet', 'Don\'t return anything to stdout.')
.option('--nointeraction', 'Do not prompt for interactive user input.')
.version(this.main.platformUtilsService.getApplicationVersion(), '-v, --version');
program.on('option:pretty', () => {
process.env.BW_PRETTY = 'true';
});
program.on('option:raw', () => {
process.env.BW_RAW = 'true';
});
program.on('option:quiet', () => {
process.env.BW_QUIET = 'true';
});
program.on('option:response', () => {
process.env.BW_RESPONSE = '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('--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 [email] [password]')
.description('Log into a user account.')
.option('--method <method>', 'Two-step login method.')
.option('--code <code>', 'Two-step login code.')
.option('--sso', 'Log in with Single-Sign On.')
.option('--passwordenv <variable-name>', 'Read password from the named environment variable.')
.option('--passwordfile <filename>', 'Read password from first line of the named file.')
.on('--help', () => {
writeLn('\n Notes:');
writeLn('');
writeLn(' See docs for valid `method` enum values.');
writeLn('');
writeLn(' Examples:');
writeLn('');
writeLn(' bwdc login');
writeLn(' bwdc login john@example.com myPassword321');
writeLn(' bwdc login john@example.com myPassword321 --method 1 --code 249213');
writeLn(' bwdc login john@example.com --passwordfile passwd.txt --method 1 --code 249213');
writeLn(' bwdc login john@example.com --passwordenv MY_PASSWD --method 1 --code 249213');
writeLn(' bwdc login --sso');
writeLn('', true);
})
.action(async (email: string, password: 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');
const response = await command.run(email, password, 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();
}
}
}

View File

@@ -1,4 +1,4 @@
$theme-colors: ( "primary": #3c8dbc, "primary-accent": #286090, "danger": #dd4b39, "success": #00a65a, "info": #555555, "warning": #bf7e16);
$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;
@@ -8,4 +8,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');
@import "~bootstrap/scss/bootstrap.scss";

View File

@@ -53,3 +53,68 @@ ul.testing-list {
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;
.callout-heading {
margin-top: 0;
}
h3.callout-heading {
font-weight: bold;
text-transform: uppercase;
}
&.callout-primary {
border-left-color: $primary;
.callout-heading {
color: $primary;
}
}
&.callout-info {
border-left-color: $info;
.callout-heading {
color: $info;
}
}
&.callout-danger {
border-left-color: $danger;
.callout-heading {
color: $danger;
}
}
&.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;
}
}

View File

@@ -12,7 +12,7 @@ import { UserEntry } from '../models/userEntry';
import { BaseDirectoryService } from './baseDirectory.service';
import { ConfigurationService } from './configuration.service';
import { DirectoryService } from './directory.service';
import { IDirectoryService } from './directory.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LogService } from 'jslib/abstractions/log.service';
@@ -20,8 +20,16 @@ import { LogService } from 'jslib/abstractions/log.service';
const NextLink = '@odata.nextLink';
const DeltaLink = '@odata.deltaLink';
const ObjectType = '@odata.type';
const UserSelectParams = '?$select=id,mail,userPrincipalName,displayName,accountEnabled';
export class AzureDirectoryService extends BaseDirectoryService implements DirectoryService {
enum UserSetType {
IncludeUser,
ExcludeUser,
IncludeGroup,
ExcludeGroup,
}
export class AzureDirectoryService extends BaseDirectoryService implements IDirectoryService {
private client: graph.Client;
private dirConfig: AzureConfiguration;
private syncConfig: SyncConfiguration;
@@ -53,20 +61,61 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force, !test);
users = await this.getCurrentUsers();
const deletedUsers = await this.getDeletedUsers(force, !test);
users = users.concat(deletedUsers);
}
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
const setFilter = await this.createAadCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
}
private async getUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
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;
}
private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
const entryIds = new Set<string>();
const entries: UserEntry[] = [];
@@ -82,11 +131,11 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
}
if (res == null) {
const userReq = this.client.api('/users/delta');
const userReq = this.client.api('/users/delta' + UserSelectParams);
res = await userReq.get();
}
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
while (true) {
const users: graphType.User[] = res.value;
if (users != null) {
@@ -95,12 +144,10 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
continue;
}
const entry = this.buildUser(user);
if (this.filterOutResult(setFilter, entry.email)) {
if (!entry.disabled && !entry.deleted) {
continue;
}
if (!entry.disabled && !entry.deleted &&
(entry.email == null || entry.email.indexOf('#') > -1)) {
if (await this.filterOutUserResult(setFilter, entry, false)) {
continue;
}
@@ -123,6 +170,130 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
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/beta/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,
checkGroupsFilter: boolean): 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);
}
// 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]),
});
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;
@@ -187,16 +358,24 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
entry.name = group.displayName;
const memReq = this.client.api('/groups/' + group.id + '/members');
const memRes = await memReq.get();
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);
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;
@@ -204,7 +383,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
private init() {
this.client = graph.Client.init({
authProvider: (done) => {
authProvider: done => {
if (this.dirConfig.applicationId == null || this.dirConfig.key == null ||
this.dirConfig.tenant == null) {
done(this.i18nService.t('dirConfigIncomplete'), null);
@@ -234,7 +413,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(data),
},
}, (res) => {
}, res => {
res.setEncoding('utf8');
res.on('data', (chunk: string) => {
const d = JSON.parse(chunk);
@@ -247,7 +426,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
done('Unknown error (' + res.statusCode + ').', null);
}
});
}).on('error', (err) => {
}).on('error', err => {
done(err, null);
});

View File

@@ -1,3 +1,5 @@
import { SyncConfiguration } from '../models/syncConfiguration';
import { GroupEntry } from '../models/groupEntry';
import { UserEntry } from '../models/userEntry';
@@ -66,21 +68,24 @@ export abstract class BaseDirectoryService {
}
protected filterUsersFromGroupsSet(users: UserEntry[], groups: GroupEntry[],
setFilter: [boolean, Set<string>]): UserEntry[] {
setFilter: [boolean, Set<string>], syncConfig: SyncConfiguration): UserEntry[] {
if (setFilter == null || users == null) {
return users;
}
return users.filter((u) => {
if (u.disabled || u.deleted) {
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;
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);
return force || (users != null && users.filter(u => !u.deleted && !u.disabled).length > 0);
}
}

View File

@@ -5,6 +5,7 @@ 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]';
@@ -13,6 +14,7 @@ const Keys = {
gsuite: 'gsuitePrivateKey',
azure: 'azureKey',
okta: 'oktaToken',
oneLogin: 'oneLoginClientSecret',
directoryConfigPrefix: 'directoryConfig_',
sync: 'syncConfig',
directoryType: 'directoryType',
@@ -25,7 +27,8 @@ const Keys = {
};
export class ConfigurationService {
constructor(private storageService: StorageService, private secureStorageService: StorageService) { }
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);
@@ -33,61 +36,77 @@ export class ConfigurationService {
return config;
}
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;
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): Promise<any> {
config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration |
OneLoginConfiguration): Promise<any> {
const savedConfig: any = Object.assign({}, config);
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;
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);
}

View File

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

View File

@@ -13,12 +13,12 @@ import { UserEntry } from '../models/userEntry';
import { BaseDirectoryService } from './baseDirectory.service';
import { ConfigurationService } from './configuration.service';
import { DirectoryService } from './directory.service';
import { IDirectoryService } from './directory.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LogService } from 'jslib/abstractions/log.service';
export class GSuiteDirectoryService extends BaseDirectoryService implements DirectoryService {
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
private client: JWT;
private service: admin_directory_v1.Admin;
private authParams: any;
@@ -49,7 +49,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
await this.auth();
let users: UserEntry[];
let users: UserEntry[] = [];
if (this.syncConfig.users) {
users = await this.getUsers();
}
@@ -57,8 +57,8 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
let groups: GroupEntry[];
if (this.syncConfig.groups) {
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
groups = await this.getGroups(setFilter);
users = this.filterUsersFromGroupsSet(users, groups, setFilter);
groups = await this.getGroups(setFilter, users);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
@@ -67,45 +67,59 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
private async getUsers(): Promise<UserEntry[]> {
const entries: UserEntry[] = [];
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
this.logService.info('Querying users.');
let p = Object.assign({ query: query }, this.authParams);
const res = await this.service.users.list(p);
if (res.status !== 200) {
throw new Error('User list API failed: ' + res.statusText);
}
let nextPageToken: string = null;
const filter = this.createCustomSet(this.syncConfig.userFilter);
if (res.data.users != null) {
for (const user of res.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
}
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);
}
const entry = this.buildUser(user, false);
if (entry != null) {
entries.push(entry);
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;
}
}
this.logService.info('Querying deleted users.');
p = Object.assign({ showDeleted: true, query: query }, 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 = 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);
}
if (delRes.data.users != null) {
for (const user of delRes.data.users) {
if (this.filterOutResult(filter, user.primaryEmail)) {
continue;
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);
}
}
}
const entry = this.buildUser(user, true);
if (entry != null) {
entries.push(entry);
}
if (nextPageToken == null) {
break;
}
}
@@ -126,53 +140,76 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
return entry;
}
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
private async getGroups(setFilter: [boolean, Set<string>], users: UserEntry[]): Promise<GroupEntry[]> {
const entries: GroupEntry[] = [];
let nextPageToken: string = null;
this.logService.info('Querying groups.');
const res = await this.service.groups.list(this.authParams);
if (res.status !== 200) {
throw new Error('Group list API failed: ' + res.statusText);
}
if (res.data.groups != null) {
for (const group of res.data.groups) {
if (!this.filterOutResult(setFilter, group.name)) {
const entry = await this.buildGroup(group);
entries.push(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 buildGroup(group: admin_directory_v1.Schema$Group) {
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;
const p = Object.assign({ groupKey: group.id }, 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;
}
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;
}
if (memRes.data.members != null) {
for (const member of memRes.data.members) {
if (member.role.toLowerCase() !== 'member') {
continue;
}
if (member.status.toLowerCase() !== 'active') {
continue;
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 (member.type.toLowerCase() === 'user') {
entry.userMemberExternalIds.add(member.id);
} else if (member.type.toLowerCase() === 'group') {
entry.groupMemberReferenceIds.add(member.id);
}
if (nextPageToken == null) {
break;
}
}
@@ -187,7 +224,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
this.client = new google.auth.JWT({
email: this.dirConfig.clientEmail,
key: this.dirConfig.privateKey,
key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null,
subject: this.dirConfig.adminUser,
scopes: [
'https://www.googleapis.com/auth/admin.directory.user.readonly',
@@ -200,9 +237,11 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
this.authParams = {
auth: this.client,
domain: this.dirConfig.domain,
};
if (this.dirConfig.customer != null) {
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;
}
}

View File

@@ -0,0 +1,25 @@
import {
deletePassword,
getPassword,
setPassword,
} from 'keytar';
import { StorageService } from 'jslib/abstractions/storage.service';
export class KeytarSecureStorageService implements StorageService {
constructor(private serviceName: string) { }
get<T>(key: string): Promise<T> {
return getPassword(this.serviceName, key).then(val => {
return JSON.parse(val) as T;
});
}
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);
}
}

View File

@@ -1,6 +1,8 @@
import * as fs from 'fs';
import * as ldap from 'ldapjs';
import { checkServerIdentity, PeerCertificate } from 'tls';
import { DirectoryType } from '../enums/directoryType';
import { GroupEntry } from '../models/groupEntry';
@@ -9,7 +11,7 @@ import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { ConfigurationService } from './configuration.service';
import { DirectoryService } from './directory.service';
import { IDirectoryService } from './directory.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LogService } from 'jslib/abstractions/log.service';
@@ -18,7 +20,7 @@ import { Utils } from 'jslib/misc/utils';
const UserControlAccountDisabled = 2;
export class LdapDirectoryService implements DirectoryService {
export class LdapDirectoryService implements IDirectoryService {
private client: ldap.Client;
private dirConfig: LdapConfiguration;
private syncConfig: SyncConfiguration;
@@ -53,7 +55,7 @@ export class LdapDirectoryService implements DirectoryService {
if (this.syncConfig.groups) {
let groupForce = force;
if (!groupForce && users != null) {
const activeUsers = users.filter((u) => !u.deleted && !u.disabled);
const activeUsers = users.filter(u => !u.deleted && !u.disabled);
groupForce = activeUsers.length > 0;
}
groups = await this.getGroups(groupForce);
@@ -231,6 +233,9 @@ export class LdapDirectoryService implements DirectoryService {
}
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='));
@@ -290,7 +295,7 @@ export class LdapDirectoryService implements DirectoryService {
const options: ldap.SearchOptions = {
filter: filter,
scope: 'sub',
paged: true,
paged: this.dirConfig.pagedSearch,
};
const entries: T[] = [];
return new Promise<T[]>((resolve, reject) => {
@@ -300,18 +305,18 @@ export class LdapDirectoryService implements DirectoryService {
return;
}
res.on('error', (resErr) => {
res.on('error', resErr => {
reject(resErr);
});
res.on('searchEntry', (entry) => {
res.on('searchEntry', entry => {
const e = processEntry(entry);
if (e != null) {
entries.push(e);
}
});
res.on('end', (result) => {
res.on('end', result => {
resolve(entries);
});
});
@@ -324,34 +329,42 @@ export class LdapDirectoryService implements DirectoryService {
reject(this.i18nService.t('dirConfigIncomplete'));
return;
}
const url = 'ldap' + (this.dirConfig.ssl ? 's' : '') + '://' + this.dirConfig.hostname +
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) {
const tlsOptions: any = {};
if (this.dirConfig.sslAllowUnauthorized != null) {
if (this.dirConfig.sslAllowUnauthorized) {
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
}
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);
}
if (Object.keys(tlsOptions).length > 0) {
options.tlsOptions = tlsOptions;
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 :
@@ -364,19 +377,35 @@ export class LdapDirectoryService implements DirectoryService {
return;
}
this.client.bind(user, pass, (err) => {
if (err != null) {
reject(err.message);
} else {
resolve();
}
});
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();
}
});
}
});
}
private async unbind(): Promise<any> {
return new Promise((resolve, reject) => {
this.client.unbind((err) => {
this.client.unbind(err => {
if (err != null) {
reject(err);
} else {
@@ -397,4 +426,23 @@ export class LdapDirectoryService implements DirectoryService {
'-' + 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

@@ -0,0 +1,29 @@
import * as fs from 'fs';
import * as lock from 'proper-lockfile';
import { LogService } from 'jslib/abstractions/log.service';
import { LowdbStorageService as LowdbStorageServiceBase } from 'jslib/services/lowdbStorage.service';
import { Utils } from 'jslib/misc/utils';
export class LowdbStorageService extends LowdbStorageServiceBase {
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();
}
}
}

View File

@@ -7,18 +7,16 @@ import { UserEntry } from '../models/userEntry';
import { BaseDirectoryService } from './baseDirectory.service';
import { ConfigurationService } from './configuration.service';
import { DirectoryService } from './directory.service';
import { IDirectoryService } from './directory.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LogService } from 'jslib/abstractions/log.service';
// tslint:disable-next-line
const okta = require('@okta/okta-sdk-nodejs');
import * as https from 'https';
export class OktaDirectoryService extends BaseDirectoryService implements DirectoryService {
export class OktaDirectoryService extends BaseDirectoryService implements IDirectoryService {
private dirConfig: OktaConfiguration;
private syncConfig: SyncConfiguration;
private client: any;
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) {
@@ -45,11 +43,6 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
throw new Error(this.i18nService.t('dirConfigIncomplete'));
}
this.client = new okta.Client({
orgUrl: this.dirConfig.orgUrl,
token: this.dirConfig.token,
});
let users: UserEntry[];
if (this.syncConfig.users) {
users = await this.getUsers(force);
@@ -59,7 +52,7 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
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);
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
}
return [groups, users];
@@ -72,12 +65,15 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
this.logService.info('Querying users.');
const usersPromise = this.client.listUsers({ filter: oktaFilter }).each((user: any) => {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
});
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;
@@ -86,12 +82,15 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
if (oktaFilter != null) {
deactOktaFilter = '(' + oktaFilter + ') and ' + deactOktaFilter;
}
deactUsersPromise = this.client.listUsers({ filter: deactOktaFilter }).each((user: any) => {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
}
});
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();
}
@@ -116,10 +115,14 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
const oktaFilter = this.buildOktaFilter(this.syncConfig.groupFilter, force, lastSync);
this.logService.info('Querying groups.');
await this.client.listGroups({ filter: oktaFilter }).each(async (group: any) => {
const entry = await this.buildGroup(group);
if (entry != null && !this.filterOutResult(setFilter, entry.name)) {
entries.push(entry);
await this.apiGetMany('groups?filter=' + this.encodeUrlParameter(oktaFilter)).then(async (groups: any[]) => {
for (const group of groups) {
const entry = await this.buildGroup(group);
if (entry != null && !this.filterOutResult(setFilter, entry.name)) {
entries.push(entry);
}
// throttle some to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 500));
}
});
return entries;
@@ -131,8 +134,10 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
entry.referenceId = group.id;
entry.name = group.profile.name;
await this.client.listGroupUsers(group.id).each((user: any) => {
entry.userMemberExternalIds.add(user.id);
await this.apiGetMany('groups/' + group.id + '/users').then((users: any[]) => {
for (const user of users) {
entry.userMemberExternalIds.add(user.id);
}
});
return entry;
@@ -152,4 +157,88 @@ export class OktaDirectoryService extends BaseDirectoryService implements Direct
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);
}
}

View File

@@ -0,0 +1,195 @@
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/abstractions/i18n.service';
import { LogService } from 'jslib/abstractions/log.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[] = [];
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) {
super();
}
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];
}
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 entry;
}
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

@@ -18,10 +18,11 @@ import { Utils } from 'jslib/misc/utils';
import { AzureDirectoryService } from './azure-directory.service';
import { ConfigurationService } from './configuration.service';
import { DirectoryService } from './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;
@@ -48,9 +49,9 @@ export class SyncService {
this.messagingService.send('dirSyncStarted');
try {
const entries = await directoryService.getEntries(force, test);
const entries = await directoryService.getEntries(force || syncConfig.overwriteExisting, test);
let groups = entries[0];
let users = entries[1];
let users = this.filterUnsupportedUsers(entries[1]);
if (groups != null && groups.length > 0) {
this.flattenUsersToGroups(groups, groups);
@@ -65,7 +66,7 @@ export class SyncService {
return [groups, users];
}
const req = this.buildRequest(groups, users, syncConfig.removeDisabled);
const req = this.buildRequest(groups, users, syncConfig.removeDisabled, syncConfig.overwriteExisting);
const reqJson = JSON.stringify(req);
let hash: string = null;
@@ -102,18 +103,25 @@ export class SyncService {
}
}
private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] {
return users == null ? null : users.filter(u => u.email == null || u.email.length <= 50);
}
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 childGroups = allGroups.filter(g => group.groupMemberReferenceIds.has(g.referenceId));
const childUsers = this.flattenUsersToGroups(childGroups, allGroups);
childUsers.forEach((id) => group.userMemberExternalIds.add(id));
childUsers.forEach(id => group.userMemberExternalIds.add(id));
allUsers = new Set([...allUsers, ...group.userMemberExternalIds]);
}
return allUsers;
}
private getDirectoryService(): DirectoryService {
private getDirectoryService(): IDirectoryService {
switch (this.dirType) {
case DirectoryType.GSuite:
return new GSuiteDirectoryService(this.configurationService, this.logService, this.i18nService);
@@ -123,13 +131,17 @@ export class SyncService {
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;
}
}
private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean): ImportDirectoryRequest {
private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean,
overwriteExisting: boolean): ImportDirectoryRequest {
const model = new ImportDirectoryRequest();
model.overwriteExisting = overwriteExisting;
if (groups != null) {
for (const g of groups) {

100
src/utils.ts Normal file
View File

@@ -0,0 +1,100 @@
import { I18nService } from 'jslib/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';
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;
}
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';
}
}
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);
});
}
}

View File

@@ -8,6 +8,7 @@
"target": "ES2016",
"allowJs": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"types": [],
"baseUrl": ".",
"paths": {
@@ -25,6 +26,9 @@
],
"node": [
"node_modules/@types/node"
],
"duo_web_sdk": [
"node_modules/duo_web_sdk"
]
}
},
@@ -34,16 +38,29 @@
"exclude": [
"node_modules",
"jslib/node_modules",
"jslib/src/services/index.ts",
"jslib/src/services/webCryptoFunction.service.ts",
"jslib/src/services/search.service.ts",
"jslib/src/services/nodeApi.service.ts",
"jslib/src/services/lowdbStorage.service.ts",
"jslib/src/services/export.service.ts",
"jslib/src/services/notifications.service.ts",
"jslib/src/services/passwordGeneration.service.ts",
"jslib/src/abstractions/index.ts",
"jslib/src/abstractions/passwordGeneration.service.ts",
"jslib/src/angular/components/export.component.ts",
"jslib/src/angular/components/register.component.ts",
"jslib/src/angular/components/add-edit.component.ts",
"jslib/src/angular/components/password-generator.component.ts",
"jslib/src/angular/components/password-generator-history.component.ts",
"jslib/src/angular/pipes/color-password.pipe.ts",
"jslib/src/angular/directives/select-copy.directive.ts",
"jslib/src/importers",
"dist",
"dist-cli",
"jslib/dist",
"build",
"build-cli",
"jslib/spec"
]
}

View File

@@ -49,6 +49,27 @@
"check-separator",
"check-type"
],
"max-classes-per-file": false
"max-classes-per-file": false,
"ordered-imports": true,
"arrow-parens": [
true,
"ban-single-arg-parens"
],
"semicolon": [
true,
"always"
],
"trailing-comma": [
true,
{
"multiline": {
"objects": "always",
"arrays": "always",
"functions": "ignore",
"typeLiterals": "ignore"
},
"singleline": "never"
}
]
}
}

80
webpack.cli.js Normal file
View File

@@ -0,0 +1,80 @@
const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
if (process.env.NODE_ENV == null) {
process.env.NODE_ENV = 'development';
}
const ENV = process.env.ENV = process.env.NODE_ENV;
const moduleRules = [
{
test: /\.ts$/,
enforce: 'pre',
loader: 'tslint-loader',
},
{
test: /\.ts$/,
loaders: ['ts-loader'],
exclude: path.resolve(__dirname, 'node_modules'),
},
{
test: /\.node$/,
loader: 'node-loader',
},
];
const plugins = [
new CleanWebpackPlugin([
path.resolve(__dirname, 'build-cli/*'),
]),
new CopyWebpackPlugin([
{ from: './src/locales', to: 'locales' },
]),
new webpack.DefinePlugin({
'process.env.BWCLI_ENV': JSON.stringify(ENV),
}),
new webpack.BannerPlugin({
banner: '#!/usr/bin/env node',
raw: true
}),
new webpack.IgnorePlugin(/^encoding$/, /node-fetch/),
];
const config = {
mode: ENV,
target: 'node',
devtool: ENV === 'development' ? 'eval-source-map' : 'source-map',
node: {
__dirname: false,
__filename: false,
},
entry: {
'bwdc': './src/bwdc.ts',
},
optimization: {
minimize: false,
},
resolve: {
extensions: ['.ts', '.js', '.json'],
alias: {
jslib: path.join(__dirname, 'jslib/src'),
tldjs: path.join(__dirname, 'jslib/src/misc/tldjs.noop'),
// ref: https://github.com/bitinn/node-fetch/issues/493
'node-fetch$': 'node-fetch/lib/index.js',
},
symlinks: false,
modules: [path.resolve('node_modules')],
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'build-cli'),
},
module: { rules: moduleRules },
plugins: plugins,
externals: [nodeExternals()],
};
module.exports = config;

View File

@@ -2,15 +2,9 @@ const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
const extractCss = new ExtractTextPlugin({
filename: '[name].css',
disable: false,
allChunks: true,
});
const common = {
module: {
rules: [
@@ -53,6 +47,7 @@ const common = {
const renderer = {
mode: 'production',
devtool: false,
target: 'electron-renderer',
node: {
__dirname: false,
@@ -93,17 +88,16 @@ const renderer = {
},
{
test: /\.scss$/,
use: extractCss.extract({
use: [
{
loader: 'css-loader',
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
},
{
loader: 'sass-loader',
},
],
publicPath: '../',
})
},
'css-loader',
'sass-loader',
],
},
// Hide System.import warnings. ref: https://github.com/angular/angular/issues/21560
{
@@ -127,11 +121,13 @@ const renderer = {
chunks: ['app/vendor', 'app/main']
}),
new webpack.SourceMapDevToolPlugin({
filename: '[name].js.map',
include: ['app/main.js']
}),
new webpack.DefinePlugin({ 'global.GENTLY': false }),
extractCss,
new MiniCssExtractPlugin({
filename: '[name].[hash].css',
chunkFilename: '[id].[hash].css',
}),
],
};