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

Compare commits

...

116 Commits

Author SHA1 Message Date
Vincent Salucci
b90694d17a [Version] Bump to 2.9.3 (#138) 2021-06-28 12:52:48 -05:00
Matt Gibson
b1b0d858ca Error on duplicate emails (#136)
* Allow main debugging in development builds

* Early fail on attempting to sync multiple users with the same email

* Truncate duplicate list if greater than 3

* Revert "Allow main debugging in development builds"

This reverts commit 3b804dd959.

(cherry picked from commit 6e7e09064f)
2021-06-24 15:35:54 -04:00
Matt Gibson
326f11be19 update jslib 2021-06-22 16:14:22 -04:00
Matt Gibson
996364f2dd Refresh token with api key (#135)
* Do not persist client creds on logout

* Override refreshing token flow with re-authentication flow

* Update jslib

* PR review comments

(cherry picked from commit 647b087fa7)
2021-06-22 16:13:42 -04:00
Thomas Rittson
4e098462dc Merge pull request #133 from bitwarden/revert-ldapjs
Revert ldapjs update, use forked repo instead
2021-06-16 16:02:52 -07:00
Thomas Rittson
0b1c2ae72a Revert ldapjs update, use forked repo instead 2021-06-15 11:10:09 +10:00
Matt Gibson
5d3fa0a0d2 Improve okta group performance (#132)
* Avoid unnecessary API calls to Okta

Filter excluded/included groups as early as possible to avoid using up
API calls and long waits

* Remove console timing calls
2021-06-11 11:10:29 -05:00
Oscar Hinton
6097bca063 Add jslib as a "real" dependency (#127)
* Split jslib

* Change hook to preinstall

* Install gyp (ci)

* Fix rebuild command

* Review comments

* Add tsconfig-paths-plugin to webpack.cli.

* Bump jslib

* Install old version of prebuild-install to bypass bug in pkg
2021-06-09 21:46:38 +02:00
Matt Gibson
56d05af07a Use organization api key for auth (#121)
* Use api key for login

* Remove user login and organization setting

* Override Api authentication to expect organization keys

* Linter fixes

* Use public API

The organization api key is valid only in the public api scope

* Use organization api key in CLI utility

* Serialize storageService writes

* Prefer multiple awaits to .then chains

* Initial PR review

* Do not treat api key inputs as passwords

This conforms with how they are handled in CLI/web

* Update jslib

* PR feedback
2021-06-02 13:43:18 -05:00
Joseph Flinn
0d17345600 constraining release to the rc branch (#126) 2021-06-01 12:01:37 -07:00
Thomas Rittson
5df62b7422 Merge pull request #122 from bitwarden/clean-exit
Add --cleanexit option
2021-05-26 21:16:39 +10:00
Thomas Rittson
868914feb1 bump juslib 2021-05-26 09:19:32 +10:00
Thomas Rittson
1a9555d4af add --cleanexit option 2021-05-25 10:59:29 +10:00
Matt Gibson
33c8f15e45 OneLogin uses Roles instead of Groups (#118) 2021-05-20 15:45:49 -05:00
Matt Gibson
ed8dd01dbd Add option to bypass large import limit of 2000 users (#119)
* Add option to bypass large import limit of 2000 users

Also add orgId to last sync hash

* Update jslib

* PR review

* Update src/services/sync.service.ts

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
2021-05-20 11:59:54 -05:00
Vince Grassia
2296e37e8f Pin versions of actions in workflow (#120) 2021-05-17 11:19:05 -04:00
Oscar Hinton
a0f33c7bdc Bump node to 14 (#116)
* Bump node to 14

* Add engines

* Bump dependencies

* Change engine to ~14.

* Bump jslib
2021-05-12 22:38:32 +02:00
Oscar Hinton
f6b249836e Bump dependencies (#114)
* Upgrade angular and webpack dependencies

* Bump microsoft-graph-client and googleapis

* Bump pkg-fetch in pipeline

* Bump jslib
2021-04-23 21:03:59 +02:00
Thomas Rittson
0c92a97054 Merge pull request #111 from bitwarden/update-ldapjs
Update ldapjs dependency
2021-04-23 08:17:47 +10:00
Vincent Salucci
24ab152559 [Version] Bump to 2.9.2 (#115)
* [Version] Bump to 2.10.0

* Downgraded to 2.9.2
2021-04-22 12:37:52 -05:00
Thomas Rittson
5f9f09d77c Merge pull request #112 from bitwarden/hotfix/expressionChangedAfterItHasBeenCheckedError
Resolve ExpressionChangedAfterItHasBeenCheckedError
2021-04-22 19:06:16 +10:00
Hinton
4d27d9e48d Update simbtn. 2021-04-22 10:09:19 +02:00
Matt Gibson
0b624b972a Update jslib (#113)
* Update jslib

* Update jslib
2021-04-21 14:24:39 -05:00
Hinton
1889e12bac Resolve ExpressionChangedAfterItHasBeenCheckedError 2021-04-21 09:35:12 +02:00
Thomas Rittson
7648f73072 Update @types/ldapjs dependency 2021-04-21 14:46:56 +10:00
Thomas Rittson
75d346ed85 update ldapjs dependency 2021-04-21 13:28:29 +10:00
Oscar Hinton
dabfe7907d Remove last remnants of old analytics code (#110) 2021-04-14 23:42:51 +02:00
Kyle Spearrin
965976223f Revert "update google apis"
This reverts commit 410f00c213.
2021-04-13 15:23:32 -04:00
Kyle Spearrin
410f00c213 update google apis 2021-04-13 15:03:20 -04:00
Kyle Spearrin
0d8b942ad4 npm audit fix 2021-04-13 14:57:46 -04:00
Kyle Spearrin
5371015a58 update libs 2021-04-13 14:56:17 -04:00
Oscar Hinton
2ead70e434 Bump jslib (#108) 2021-04-07 20:42:39 +02:00
Thomas Rittson
ffca14cb5f Merge pull request #107 from bitwarden/fix-ad-not-removing-users
Fix AD sync not overwriting removed users
2021-04-01 06:48:32 +10:00
Thomas Rittson
090d5e82df Send empty sync to server if overwriteExisting 2021-03-30 12:37:48 +10:00
Chad Scharf
8893ddf0f7 Merge pull request #106 from djsmith85/master
Fix filteringForUnsupportedUsers and extend email validation to handle emails up to 256 char
2021-03-25 14:23:26 -04:00
Daniel James Smith
997ec5a699 Extend validation to handle emails up to 256 char 2021-03-25 18:39:05 +01:00
Daniel James Smith
762818ee39 Fix filtering unsupported users 2021-03-25 18:36:43 +01:00
Oscar Hinton
61c6ba8189 Bump electron to 11.3.0 (#104) 2021-03-15 23:19:30 +01:00
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
Pasi Niemi
3840bce6d7 Enable filtering Azure AD directory by administrative unit 2020-05-19 08:00:25 +03:00
80 changed files with 26582 additions and 12172 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.

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

@@ -0,0 +1,414 @@
name: Build
on:
push:
branches-ignore:
- 'l10n_master'
workflow_dispatch:
inputs:
jobs:
cloc:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Setup Windows builder
run: |
choco install checksum --no-progress
choco install reshack --no-progress
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14.x'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
- name: Setting WIN_PKG
run: |
echo "WIN_PKG=$env:WIN_PKG" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "version: $env:pkgVersion"
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v3.0\fetched-v14.16.1-win-x64
- name: get pkg-fetch
shell: pwsh
run: |
cd $HOME
$fetchedUrl = "https://github.com/vercel/pkg-fetch/releases/download/v3.0/node-v14.16.1-win-x64"
New-Item -ItemType directory -Path ./.pkg-cache
New-Item -ItemType directory -Path ./.pkg-cache/v3.0
Invoke-RestMethod -Uri $fetchedUrl -OutFile "./.pkg-cache/v3.0/fetched-v14.16.1-win-x64"
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v3.0\fetched-v14.16.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@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
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@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
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@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
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@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
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@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
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@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
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@a71d1eb2c86af85faa8c772c03fb365e377e45ea
with:
dotnet-version: "3.1.x"
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14.x'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
- 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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
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@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
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@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14.x'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
- 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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
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@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14.x'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
- 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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
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@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
with:
name: Bitwarden-Connector-${{ env.PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env.PACKAGE_VERSION }}.dmg

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

@@ -0,0 +1,425 @@
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: Branch check
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]]; then
echo "==================================="
echo "[!] Can only release from rc branch"
echo "==================================="
exit 1
fi
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e
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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Setup Windows builder
run: |
choco install checksum --no-progress
choco install reshack --no-progress
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14.x'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
- 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\v3.0\fetched-v14.16.1-win-x64
- name: get pkg-fetch
shell: pwsh
run: |
cd $HOME
$fetchedUrl = "https://github.com/vercel/pkg-fetch/releases/download/v3.0/node-v14.16.1-win-x64"
New-Item -ItemType directory -Path ./.pkg-cache
New-Item -ItemType directory -Path ./.pkg-cache/v3.0
Invoke-RestMethod -Uri $fetchedUrl -OutFile "./.pkg-cache/v3.0/fetched-v14.16.1-win-x64"
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v3.0\fetched-v14.16.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@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5
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@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5
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@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5
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@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5
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@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5
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@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5
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@a71d1eb2c86af85faa8c772c03fb365e377e45ea
with:
dotnet-version: "3.1.x"
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14.x'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
- 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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14.x'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
- 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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14.x'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
- 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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- 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 }}

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

View File

@@ -1,162 +0,0 @@
image:
- Visual Studio 2017
- Ubuntu1804
branches:
except:
- l10n_master
environment:
WIN_PKG: C:\Users\appveyor\.pkg-cache\v2.5\fetched-v10.4.1-win-x64
stack: node 10
init:
- ps: |
if($isWindows -and $env:DEBUG_RDP -eq "true") {
iex ((new-object net.webclient).DownloadString(`
'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
}
- sh: sudo apt-get update
- sh: sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev rpm
- ps: |
if($isWindows) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-Product node 10
$env:PATH = "C:\Program Files (x86)\Resource Hacker;${env:PATH}"
}
if($env:APPVEYOR_REPO_TAG -eq "true") {
$env:RELEASE_NAME = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v")
}
install:
- ps: |
$env:PACKAGE_VERSION = (Get-Content -Raw -Path .\src\package.json | ConvertFrom-Json).version
$env:PROD_DEPLOY = "false"
if($env:APPVEYOR_REPO_TAG -eq "true" -and $env:APPVEYOR_RE_BUILD -eq "True") {
$env:PROD_DEPLOY = "true"
echo "This is a production deployment."
}
if($isWindows) {
if(Test-Path -Path $env:WIN_PKG) {
$env:VER_INFO = "true"
}
choco install reshack --no-progress
choco install cloc --no-progress
choco install checksum --no-progress
cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
.\make-versioninfo.ps1
}
- ps: |
if($isWindows) {
$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"
}
before_build:
- node --version
- npm --version
build_script:
- cmd: |
if defined VER_INFO ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action delete -mask ICONGROUP,1,
if defined VER_INFO ResourceHacker -open version-info.rc -save version-info.res -action compile
if defined VER_INFO ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action addoverwrite -resource version-info.res
- sh: npm install
- sh: npm run rebuild
- sh: npm run dist:lin
- cmd: npm install
- cmd: npm run rebuild
- cmd: npm run dist:win:ci
- cmd: npm run reset
- cmd: npm run dist:cli
- cmd: 7z a ./dist-cli/bwdc-windows-%PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node
- cmd: 7z a ./dist-cli/bwdc-macos-%PACKAGE_VERSION%.zip ./dist-cli/macos/bwdc ./keytar/macos/keytar.node
- cmd: 7z a ./dist-cli/bwdc-linux-%PACKAGE_VERSION%.zip ./dist-cli/linux/bwdc ./keytar/linux/keytar.node
- ps: |
if($isWindows) {
Expand-Archive -Path "./dist-cli/bwdc-windows-${env:PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
$testVersion = Invoke-Expression '& ./test/windows/bwdc.exe -v'
if($testVersion -ne $env:PACKAGE_VERSION) {
Throw "Version test failed."
}
}
- ps: |
if($isWindows) {
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
}
- ps: |
if($isLinux) {
Push-AppveyorArtifact ./dist/Bitwarden-Connector-${env:PACKAGE_VERSION}-x86_64.AppImage
}
else {
Push-AppveyorArtifact .\dist\Bitwarden-Connector-Portable-${env:PACKAGE_VERSION}.exe
Push-AppveyorArtifact .\dist\Bitwarden-Connector-Installer-${env:PACKAGE_VERSION}.exe
Push-AppveyorArtifact .\dist-cli\bwdc-windows-${env:PACKAGE_VERSION}.zip
Push-AppveyorArtifact .\dist-cli\bwdc-macos-${env:PACKAGE_VERSION}.zip
Push-AppveyorArtifact .\dist-cli\bwdc-linux-${env:PACKAGE_VERSION}.zip
Push-AppveyorArtifact .\dist-cli\bwdc-windows-sha256-${env:PACKAGE_VERSION}.txt
Push-AppveyorArtifact .\dist-cli\bwdc-macos-sha256-${env:PACKAGE_VERSION}.txt
Push-AppveyorArtifact .\dist-cli\bwdc-linux-sha256-${env:PACKAGE_VERSION}.txt
}
on_finish:
- ps: |
if($isWindows -and $env:DEBUG_RDP -eq "true") {
$blockRdp = $true
iex ((new-object net.webclient).DownloadString(`
'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
}
for:
-
matrix:
only:
- image: Visual Studio 2017
cache:
- '%LOCALAPPDATA%\electron'
- '%LOCALAPPDATA%\electron-builder'
- 'C:\Users\appveyor\.pkg-cache\'
-
matrix:
only:
- image: Ubuntu1804
cache:
- '/home/appveyor/.cache/electron'
- '/home/appveyor/.cache/electron-builder'
deploy:
tag: $(APPVEYOR_REPO_TAG_NAME)
release: $(RELEASE_NAME)
provider: GitHub
auth_token: $(GH_TOKEN)
artifact: /.*\.(zip|txt)/,
force_update: true
on:
branch: master
APPVEYOR_REPO_TAG: true

View File

@@ -20,12 +20,6 @@ function webfonts() {
.pipe(gulp.dest(paths.cssDir));
}
// ref: https://github.com/angular/angular/issues/22524
function cleanupAotIssue() {
return del(['./node_modules/@types/uglify-js/node_modules/source-map/source-map.d.ts']);
}
exports.clean = clean;
exports.cleanupAotIssue = cleanupAotIssue;
exports.webfonts = gulp.series(clean, webfonts);
exports['prebuild:renderer'] = gulp.parallel(webfonts, cleanupAotIssue);;
exports['prebuild:renderer'] = webfonts;;

2
jslib

Submodule jslib updated: 212a2e3745...55a9ea9e18

35374
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,14 +21,14 @@
"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": "npm run sub:init",
"preinstall": "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",
"rebuild": "electron-rebuild",
"reset": "rimraf ./node_modules/keytar/* && npm install",
"lint": "tslint src/**/*.ts || true",
"lint:fix": "tslint src/**/*.ts --fix",
"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",
@@ -46,9 +46,9 @@
"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 . --targets win-x64 --output ./dist-cli/windows/bwdc.exe",
"pack:cli:mac": "pkg . --targets macos-x64 --output ./dist-cli/macos/bwdc",
"pack:cli:lin": "pkg . --targets linux-x64 --output ./dist-cli/linux/bwdc",
"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",
@@ -85,7 +85,8 @@
"target": [
"portable",
"nsis"
]
],
"sign": "scripts/sign.js"
},
"linux": {
"category": "Utility",
@@ -130,98 +131,69 @@
"artifactName": "Bitwarden-Connector-${version}-${arch}.${ext}"
}
},
"bin": {
"bwdc": "./build-cli/bwdc.js"
},
"pkg": {
"assets": "./build-cli/**/*"
},
"devDependencies": {
"@angular/compiler-cli": "^7.2.11",
"@angular/compiler-cli": "^11.2.11",
"@microsoft/microsoft-graph-types": "^1.4.0",
"@ngtools/webpack": "^7.2.2",
"@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.3.3",
"@types/node": "^10.9.4",
"@types/node-fetch": "^2.1.2",
"@types/node-forge": "^0.7.5",
"@types/papaparse": "^4.5.3",
"@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": "6.1.7",
"electron-builder": "22.4.0",
"electron-notarize": "^0.2.1",
"electron-rebuild": "^1.9.0",
"@ngtools/webpack": "^11.2.10",
"@types/ldapjs": "^1.0.10",
"@types/node": "^14.14.43",
"@types/proper-lockfile": "^4.1.1",
"clean-webpack-plugin": "^3.0.0",
"concurrently": "^6.0.2",
"copy-webpack-plugin": "^6.4.0",
"cross-env": "^7.0.3",
"css-loader": "^5.2.4",
"del": "^6.0.0",
"electron-builder": "^22.10.5",
"electron-notarize": "^1.0.0",
"electron-rebuild": "^2.3.5",
"electron-reload": "^1.5.0",
"mini-css-extract-plugin": "^0.9.0",
"file-loader": "^2.0.0",
"file-loader": "^6.2.0",
"font-awesome": "4.7.0",
"gulp": "^4.0.0",
"gulp-google-webfonts": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"node-abi": "^2.9.0",
"node-loader": "^0.6.0",
"node-sass": "^4.13.1",
"pkg": "4.3.4",
"rimraf": "^2.6.2",
"sass-loader": "^7.1.0",
"ts-loader": "^5.3.3",
"tslint": "^5.12.1",
"gulp": "^4.0.2",
"gulp-google-webfonts": "^4.0.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.1",
"mini-css-extract-plugin": "^1.5.0",
"node-loader": "^1.0.3",
"pkg": "^5.1.0",
"rimraf": "^3.0.2",
"sass": "^1.32.11",
"sass-loader": "^10.1.1",
"tapable": "^1.1.3",
"ts-loader": "^8.1.0",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"tslint": "~6.1.0",
"tslint-loader": "^3.5.4",
"typescript": "3.2.4",
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1",
"webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2"
"typescript": "4.1.5",
"webpack": "^4.46.0",
"webpack-cli": "^4.6.0",
"webpack-merge": "^5.7.3",
"webpack-node-externals": "^3.0.0",
"prebuild-install": "^5.0.0"
},
"dependencies": {
"@angular/animations": "7.2.1",
"@angular/common": "7.2.1",
"@angular/compiler": "7.2.1",
"@angular/core": "7.2.1",
"@angular/forms": "7.2.1",
"@angular/platform-browser": "7.2.1",
"@angular/platform-browser-dynamic": "7.2.1",
"@angular/router": "7.2.1",
"@angular/upgrade": "7.2.1",
"@microsoft/microsoft-graph-client": "1.2.0",
"@okta/okta-sdk-nodejs": "2.0.1",
"angular2-toaster": "6.1.0",
"angulartics2": "6.3.0",
"big-integer": "1.6.36",
"bootstrap": "4.3.1",
"chalk": "2.4.1",
"commander": "2.18.0",
"core-js": "2.6.2",
"@bitwarden/jslib-angular": "file:jslib/angular",
"@bitwarden/jslib-common": "file:jslib/common",
"@bitwarden/jslib-electron": "file:jslib/electron",
"@bitwarden/jslib-node": "file:jslib/node",
"@microsoft/microsoft-graph-client": "^2.2.1",
"angular2-toaster": "^11.0.1",
"bootstrap": "^4.6.0",
"chalk": "^4.1.1",
"commander": "^7.2.0",
"core-js": "^3.11.0",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"electron-log": "2.2.17",
"electron-store": "1.3.0",
"electron-updater": "4.2.0",
"form-data": "2.3.2",
"googleapis": "43.0.0",
"https-proxy-agent": "4.0.0",
"inquirer": "6.2.0",
"keytar": "4.13.0",
"form-data": "^4.0.0",
"googleapis": "^73.0.0",
"inquirer": "8.0.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.3",
"zone.js": "0.8.28"
"lunr": "^2.3.9",
"open": "^8.0.6",
"proper-lockfile": "^4.1.2"
},
"engines": {
"node": "~14",
"npm": "~7"
}
}

View File

@@ -6,12 +6,13 @@ exports.default = async function notarizing(context) {
if (electronPlatformName !== 'darwin') {
return;
}
const appleId = process.env.APPLEID;
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: `@keychain:AC_PASSWORD`,
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.6.0"
}
}

View File

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

View File

@@ -0,0 +1,80 @@
import {
Component,
ComponentFactoryResolver,
Input,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { Router } from '@angular/router';
import { EnvironmentComponent } from './environment.component';
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ModalComponent } from 'jslib-angular/components/modal.component';
import { Utils } from 'jslib-common/misc/utils';
import { ConfigurationService } from '../../services/configuration.service';
@Component({
selector: 'app-apiKey',
templateUrl: 'apiKey.component.html',
})
export class ApiKeyComponent {
@ViewChild('environment', { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef;
@Input() clientId: string = '';
@Input() clientSecret: string = '';
formPromise: Promise<any>;
successRoute = '/tabs/dashboard';
constructor(private authService: AuthService, private apiKeyService: ApiKeyService, private router: Router,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private configurationService: ConfigurationService, private platformUtilsService: PlatformUtilsService) { }
async submit() {
if (this.clientId == null || this.clientId === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('clientIdRequired'));
return;
}
if (!this.clientId.startsWith('organization')) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('orgApiKeyRequired'));
return;
}
if (this.clientSecret == null || this.clientSecret === '') {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('clientSecretRequired'));
return;
}
const idParts = this.clientId.split('.');
if (idParts.length !== 2 || idParts[0] !== 'organization' || !Utils.isGuid(idParts[1])) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidClientId'));
return;
}
try {
this.formPromise = this.authService.logInApiKey(this.clientId, this.clientSecret);
await this.formPromise;
const organizationId = await this.apiKeyService.getEntityId();
await this.configurationService.saveOrganizationId(organizationId);
this.router.navigate([this.successRoute]);
} catch { }
}
settings() {
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
const modal = this.environmentModal.createComponent(factory).instance;
const childComponent = modal.show<EnvironmentComponent>(EnvironmentComponent,
this.environmentModal);
childComponent.onSaved.subscribe(() => {
modal.close();
});
}
}

View File

@@ -17,6 +17,11 @@
</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">

View File

@@ -1,10 +1,10 @@
import { Component } from '@angular/core';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib/angular/components/environment.component';
import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib-angular/components/environment.component';
@Component({
selector: 'app-environment',

View File

@@ -1,35 +0,0 @@
<div class="container-fluid">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<p class="text-center font-weight-bold">{{'welcome' | i18n}}</p>
<p class="text-center">{{'logInDesc' | i18n}}</p>
<div class="card">
<h5 class="card-header">{{'logIn' | i18n}}</h5>
<div class="card-body">
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" type="text" name="Email" [(ngModel)]="email" class="form-control">
</div>
<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">
</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>
</div>
</div>
</div>
</form>
</div>
<ng-template #environment></ng-template>

View File

@@ -1,45 +0,0 @@
import {
Component,
ComponentFactoryResolver,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { Router } from '@angular/router';
import { EnvironmentComponent } from './environment.component';
import { AuthService } from 'jslib/abstractions/auth.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 { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/login.component';
import { ModalComponent } from 'jslib/angular/components/modal.component';
@Component({
selector: 'app-login',
templateUrl: 'login.component.html',
})
export class LoginComponent extends BaseLoginComponent {
@ViewChild('environment', { read: ViewContainerRef }) environmentModal: ViewContainerRef;
constructor(authService: AuthService, router: Router,
i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
storageService: StorageService, platformUtilsService: PlatformUtilsService,
stateService: StateService) {
super(authService, router, platformUtilsService, i18nService, storageService, stateService);
super.successRoute = '/tabs/dashboard';
}
settings() {
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
const modal = this.environmentModal.createComponent(factory).instance;
const childComponent = modal.show<EnvironmentComponent>(EnvironmentComponent,
this.environmentModal);
childComponent.onSaved.subscribe(() => {
modal.close();
});
}
}

View File

@@ -1,26 +0,0 @@
<div class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{'twoStepOptions' | i18n}}</h3>
<button type="button" class="close" data-dismiss="modal" title="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p *ngFor="let p of providers">
<a href="#" appStopClick (click)="choose(p)">
<strong>{{p.name}}</strong>
</a>
<br /> {{p.description}}
</p>
<p>
<a href="#" (click)="recover()">
<strong>{{'recoveryCodeTitle' | i18n}}</strong>
</a>
<br /> {{'recoveryCodeDesc' | i18n}}
</p>
</div>
</div>
</div>
</div>

View File

@@ -1,21 +0,0 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from 'jslib/abstractions/auth.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import {
TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent,
} from 'jslib/angular/components/two-factor-options.component';
@Component({
selector: 'app-two-factor-options',
templateUrl: 'two-factor-options.component.html',
})
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
constructor(authService: AuthService, router: Router,
i18nService: I18nService, platformUtilsService: PlatformUtilsService) {
super(authService, router, i18nService, platformUtilsService, window);
}
}

View File

@@ -1,70 +0,0 @@
<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">
<h5 class="card-header">{{title}}</h5>
<div class="card-body">
<ng-container
*ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
<p *ngIf="selectedProviderType === providerType.Authenticator">
{{'enterVerificationCodeApp' | i18n}}
</p>
<p *ngIf="selectedProviderType === providerType.Email">
{{'enterVerificationCodeEmail' | i18n : twoFactorEmail}}
</p>
<div class="form-group">
<label for="code">{{'verificationCode' | i18n}}</label>
<input id="code" type="text" name="Code" [(ngModel)]="token" appAutofocus
class="form-control">
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
<p>{{'insertYubiKey' | i18n}}</p>
<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">
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo">
<div id="duo-frame">
<iframe id="duo_iframe"></iframe>
</div>
</ng-container>
<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">
<label class="form-check-label" for="remember">{{'rememberMe' | i18n}}</label>
</div>
</div>
<ng-container class="card-body"
*ngIf="selectedProviderType === null || selectedProviderType === providerType.U2f">
<p>{{'noTwoStepProviders' | i18n}}</p>
<p>{{'noTwoStepProviders2' | i18n}}</p>
</ng-container>
<button type="submit" class="btn btn-primary" [disabled]="form.loading" *ngIf="selectedProviderType != null && selectedProviderType !== providerType.U2f && selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo">
<i class="fa fa-sign-in fa-fw" [hidden]="form.loading"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!form.loading"></i>
{{'continue' | i18n}}
</button>
<a routerLink="/login" class="btn btn-link">{{'cancel' | i18n}}</a>
</div>
</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">
{{'sendVerificationCodeEmailAgain' | i18n}}
</a>
</div>
</div>
</div>
</form>
</div>
<ng-template #twoFactorOptions></ng-template>

View File

@@ -1,57 +0,0 @@
import {
Component,
ComponentFactoryResolver,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { Router } from '@angular/router';
import { TwoFactorOptionsComponent } from './two-factor-options.component';
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
import { ApiService } from 'jslib/abstractions/api.service';
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';
@Component({
selector: 'app-two-factor',
templateUrl: 'two-factor.component.html',
})
export class TwoFactorComponent extends BaseTwoFactorComponent {
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
constructor(authService: AuthService, router: Router,
i18nService: I18nService, apiService: ApiService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
private componentFactoryResolver: ComponentFactoryResolver, stateService: StateService,
storageService: StorageService) {
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
stateService, storageService);
super.successRoute = '/tabs/dashboard';
}
anotherMethod() {
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
const modal = this.twoFactorOptionsModal.createComponent(factory).instance;
const childComponent = modal.show<TwoFactorOptionsComponent>(TwoFactorOptionsComponent,
this.twoFactorOptionsModal);
childComponent.onProviderSelected.subscribe(async (provider: TwoFactorProviderType) => {
modal.close();
this.selectedProviderType = provider;
await this.init();
});
childComponent.onRecoverSelected.subscribe(() => {
modal.close();
});
}
}

View File

@@ -7,8 +7,7 @@ import {
import { AuthGuardService } from './services/auth-guard.service';
import { LaunchGuardService } from './services/launch-guard.service';
import { LoginComponent } from './accounts/login.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { ApiKeyComponent } from './accounts/apiKey.component';
import { DashboardComponent } from './tabs/dashboard.component';
import { MoreComponent } from './tabs/more.component';
import { SettingsComponent } from './tabs/settings.component';
@@ -18,10 +17,9 @@ const routes: Routes = [
{ path: '', redirectTo: '/login', pathMatch: 'full' },
{
path: 'login',
component: LoginComponent,
component: ApiKeyComponent,
canActivate: [LaunchGuardService],
},
{ path: '2fa', component: TwoFactorComponent },
{
path: 'tabs',
component: TabsComponent,

View File

@@ -5,8 +5,6 @@ import {
ToasterContainerComponent,
ToasterService,
} from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import {
Component,
@@ -21,18 +19,18 @@ import {
import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { ModalComponent } from 'jslib/angular/components/modal.component';
import { ModalComponent } from 'jslib-angular/components/modal.component';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
import { ApiService } from 'jslib/abstractions/api.service';
import { AuthService } from 'jslib/abstractions/auth.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { UserService } from 'jslib/abstractions/user.service';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ConfigurationService } from '../services/configuration.service';
import { SyncService } from '../services/sync.service';
@@ -48,7 +46,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,
@@ -60,10 +58,9 @@ export class AppComponent implements OnInit {
private lastActivity: number = null;
private modal: ModalComponent = null;
constructor(private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
private broadcasterService: BroadcasterService, private userService: UserService,
constructor(private broadcasterService: BroadcasterService, private userService: UserService,
private tokenService: TokenService, private storageService: StorageService,
private authService: AuthService, private router: Router, private analytics: Angulartics2,
private authService: AuthService, private router: Router,
private toasterService: ToasterService, private i18nService: I18nService,
private sanitizer: DomSanitizer, private ngZone: NgZone,
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
@@ -76,12 +73,6 @@ export class AppComponent implements OnInit {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case 'loggedIn':
if (await this.userService.isAuthenticated()) {
const profile = await this.apiService.getProfile();
this.stateService.save('profileOrganizations', profile.organizations);
}
break;
case 'syncScheduleStarted':
case 'syncScheduleStopped':
this.stateService.save('syncingDir', message.command === 'syncScheduleStarted');
@@ -127,11 +118,8 @@ export class AppComponent implements OnInit {
case 'showToast':
this.showToast(message);
break;
case 'analyticsEventTrack':
this.analytics.eventTrack.next({
action: message.action,
properties: { label: message.label },
});
case 'ssoCallback':
this.router.navigate(['sso'], { queryParams: { code: message.code, state: message.state } });
break;
default:
}
@@ -146,13 +134,10 @@ export class AppComponent implements OnInit {
private async logOut(expired: boolean) {
const userId = await this.userService.getUserId();
await Promise.all([
this.tokenService.clearToken(),
this.userService.clear(),
]);
await this.tokenService.clearToken();
await this.userService.clear();
this.authService.logOut(async () => {
this.analytics.eventTrack.next({ action: 'Logged Out' });
if (expired) {
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
this.i18nService.t('loginExpired'));

View File

@@ -1,9 +1,7 @@
import 'core-js';
import 'core-js/stable';
import 'zone.js/dist/zone';
import { ToasterModule } from 'angular2-toaster';
import { Angulartics2Module } from 'angulartics2';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import { AppRoutingModule } from './app-routing.module';
import { ServicesModule } from './services/services.module';
@@ -15,30 +13,28 @@ 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 { CalloutComponent } from 'jslib-angular/components/callout.component';
import { IconComponent } from 'jslib-angular/components/icon.component';
import { ModalComponent } from 'jslib-angular/components/modal.component';
import { ApiKeyComponent } from './accounts/apiKey.component';
import { EnvironmentComponent } from './accounts/environment.component';
import { LoginComponent } from './accounts/login.component';
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
import { TwoFactorComponent } from './accounts/two-factor.component';
import { DashboardComponent } from './tabs/dashboard.component';
import { MoreComponent } from './tabs/more.component';
import { SettingsComponent } from './tabs/settings.component';
import { TabsComponent } from './tabs/tabs.component';
import { A11yTitleDirective } from 'jslib/angular/directives/a11y-title.directive';
import { ApiActionDirective } from 'jslib/angular/directives/api-action.directive';
import { AutofocusDirective } from 'jslib/angular/directives/autofocus.directive';
import { BlurClickDirective } from 'jslib/angular/directives/blur-click.directive';
import { BoxRowDirective } from 'jslib/angular/directives/box-row.directive';
import { FallbackSrcDirective } from 'jslib/angular/directives/fallback-src.directive';
import { StopClickDirective } from 'jslib/angular/directives/stop-click.directive';
import { StopPropDirective } from 'jslib/angular/directives/stop-prop.directive';
import { A11yTitleDirective } from 'jslib-angular/directives/a11y-title.directive';
import { ApiActionDirective } from 'jslib-angular/directives/api-action.directive';
import { AutofocusDirective } from 'jslib-angular/directives/autofocus.directive';
import { BlurClickDirective } from 'jslib-angular/directives/blur-click.directive';
import { BoxRowDirective } from 'jslib-angular/directives/box-row.directive';
import { FallbackSrcDirective } from 'jslib-angular/directives/fallback-src.directive';
import { StopClickDirective } from 'jslib-angular/directives/stop-click.directive';
import { StopPropDirective } from 'jslib-angular/directives/stop-prop.directive';
import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
import { I18nPipe } from 'jslib-angular/pipes/i18n.pipe';
import { SearchCiphersPipe } from 'jslib-angular/pipes/search-ciphers.pipe';
@NgModule({
imports: [
@@ -47,16 +43,12 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
FormsModule,
AppRoutingModule,
ServicesModule,
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
pageTracking: {
clearQueryParams: true,
},
}),
ToasterModule.forRoot(),
],
declarations: [
A11yTitleDirective,
ApiActionDirective,
ApiKeyComponent,
AppComponent,
AutofocusDirective,
BlurClickDirective,
@@ -67,7 +59,6 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
FallbackSrcDirective,
I18nPipe,
IconComponent,
LoginComponent,
ModalComponent,
MoreComponent,
SearchCiphersPipe,
@@ -75,13 +66,10 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
StopClickDirective,
StopPropDirective,
TabsComponent,
TwoFactorComponent,
TwoFactorOptionsComponent,
],
entryComponents: [
EnvironmentComponent,
ModalComponent,
TwoFactorOptionsComponent,
],
providers: [],
bootstrap: [AppComponent],

View File

@@ -1,16 +0,0 @@
import { NgModule } from '@angular/core';
import { InputVerbatimDirective } from 'jslib/angular/directives/input-verbatim.directive';
import { TrueFalseValueDirective } from 'jslib/angular/directives/true-false-value.directive';
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
@NgModule({
imports: [],
declarations: [
InputVerbatimDirective,
TrueFalseValueDirective,
SearchPipe,
],
})
export class DummyModule {
}

View File

@@ -1,7 +1,7 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { isDev } from 'jslib/electron/utils';
import { isDev } from 'jslib-electron/utils';
// tslint:disable-next-line
require('../scss/styles.scss');

View File

@@ -3,17 +3,17 @@ import {
CanActivate,
Router,
} from '@angular/router';
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { UserService } from 'jslib/abstractions/user.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
@Injectable()
export class AuthGuardService implements CanActivate {
constructor(private userService: UserService, private router: Router,
constructor(private apiKeyService: ApiKeyService, private router: Router,
private messagingService: MessagingService) { }
async canActivate() {
const isAuthed = await this.userService.isAuthenticated();
const isAuthed = await this.apiKeyService.isAuthenticated();
if (!isAuthed) {
this.messagingService.send('logout');
return false;

View File

@@ -4,14 +4,14 @@ import {
Router,
} from '@angular/router';
import { UserService } from 'jslib/abstractions/user.service';
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
@Injectable()
export class LaunchGuardService implements CanActivate {
constructor(private userService: UserService, private router: Router) { }
constructor(private apiKeyService: ApiKeyService, private router: Router) { }
async canActivate() {
const isAuthed = await this.userService.isAuthenticated();
const isAuthed = await this.apiKeyService.isAuthenticated();
if (!isAuthed) {
return true;
}

View File

@@ -1,5 +1,3 @@
import { remote } from 'electron';
import {
APP_INITIALIZER,
NgModule,
@@ -7,11 +5,11 @@ import {
import { ToasterModule } from 'angular2-toaster';
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 { ElectronLogService } from 'jslib-electron/services/electronLog.service';
import { ElectronPlatformUtilsService } from 'jslib-electron/services/electronPlatformUtils.service';
import { ElectronRendererMessagingService } from 'jslib-electron/services/electronRendererMessaging.service';
import { ElectronRendererSecureStorageService } from 'jslib-electron/services/electronRendererSecureStorage.service';
import { ElectronRendererStorageService } from 'jslib-electron/services/electronRendererStorage.service';
import { AuthGuardService } from './auth-guard.service';
import { LaunchGuardService } from './launch-guard.service';
@@ -20,64 +18,78 @@ import { ConfigurationService } from '../../services/configuration.service';
import { I18nService } from '../../services/i18n.service';
import { SyncService } from '../../services/sync.service';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
import { ValidationService } from 'jslib/angular/services/validation.service';
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
import { ValidationService } from 'jslib-angular/services/validation.service';
import { Analytics } from 'jslib/misc/analytics';
import { ApiKeyService } from 'jslib-common/services/apiKey.service';
import { AppIdService } from 'jslib-common/services/appId.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
import { ContainerService } from 'jslib-common/services/container.service';
import { CryptoService } from 'jslib-common/services/crypto.service';
import { EnvironmentService } from 'jslib-common/services/environment.service';
import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service';
import { PolicyService } from 'jslib-common/services/policy.service';
import { StateService } from 'jslib-common/services/state.service';
import { TokenService } from 'jslib-common/services/token.service';
import { UserService } from 'jslib-common/services/user.service';
import { ApiService } from 'jslib/services/api.service';
import { AppIdService } from 'jslib/services/appId.service';
import { AuthService } from 'jslib/services/auth.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 { NodeCryptoFunctionService } from 'jslib/services/nodeCryptoFunction.service';
import { StateService } from 'jslib/services/state.service';
import { TokenService } from 'jslib/services/token.service';
import { UserService } from 'jslib/services/user.service';
import { NodeCryptoFunctionService } from 'jslib-node/services/nodeCryptoFunction.service';
import { ApiService as ApiServiceAbstraction } from 'jslib/abstractions/api.service';
import { AppIdService as AppIdServiceAbstraction } from 'jslib/abstractions/appId.service';
import { AuthService as AuthServiceAbstraction } from 'jslib/abstractions/auth.service';
import { CryptoService as CryptoServiceAbstraction } from 'jslib/abstractions/crypto.service';
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib/abstractions/cryptoFunction.service';
import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib/abstractions/environment.service';
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 { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.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';
import { UserService as UserServiceAbstraction } from 'jslib/abstractions/user.service';
import { ApiService as ApiServiceAbstraction } from 'jslib-common/abstractions/api.service';
import { ApiKeyService as ApiKeyServiceAbstraction } from 'jslib-common/abstractions/apiKey.service';
import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service';
import { CryptoService as CryptoServiceAbstraction } from 'jslib-common/abstractions/crypto.service';
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib-common/abstractions/cryptoFunction.service';
import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib-common/abstractions/environment.service';
import { I18nService as I18nServiceAbstraction } from 'jslib-common/abstractions/i18n.service';
import { LogService as LogServiceAbstraction } from 'jslib-common/abstractions/log.service';
import { MessagingService as MessagingServiceAbstraction } from 'jslib-common/abstractions/messaging.service';
import {
PasswordGenerationService as PasswordGenerationServiceAbstraction,
} from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService as PolicyServiceAbstraction } from 'jslib-common/abstractions/policy.service';
import { StateService as StateServiceAbstraction } from 'jslib-common/abstractions/state.service';
import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service';
import { TokenService as TokenServiceAbstraction } from 'jslib-common/abstractions/token.service';
import { UserService as UserServiceAbstraction } from 'jslib-common/abstractions/user.service';
import { ApiService, refreshToken } from '../../services/api.service';
import { AuthService } from '../../services/auth.service';
const logService = new ElectronLogService();
const i18nService = new I18nService(window.navigator.language, './locales');
const stateService = new StateService();
const broadcasterService = new BroadcasterService();
const messagingService = new ElectronRendererMessagingService(broadcasterService);
const platformUtilsService = new ElectronPlatformUtilsService(i18nService, messagingService, true);
const storageService: StorageServiceAbstraction = new ElectronStorageService(remote.app.getPath('userData'));
const storageService: StorageServiceAbstraction = new ElectronRendererStorageService();
const platformUtilsService = new ElectronPlatformUtilsService(i18nService, messagingService, false, storageService);
const secureStorageService: StorageServiceAbstraction = new ElectronRendererSecureStorageService();
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new NodeCryptoFunctionService();
const cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService);
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,
const apiService = new ApiService(tokenService, platformUtilsService, refreshTokenCallback,
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
const environmentService = new EnvironmentService(apiService, storageService, null);
const userService = new UserService(tokenService, storageService);
const apiKeyService = new ApiKeyService(tokenService, storageService);
const containerService = new ContainerService(cryptoService);
const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
i18nService, platformUtilsService, messagingService, false);
i18nService, platformUtilsService, messagingService, null, logService, apiKeyService, 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);
function refreshTokenCallback(): Promise<any> {
return refreshToken(apiKeyService, authService);
}
export function initFactory(): Function {
return async () => {
await environmentService.setUrlsFromStorage();
@@ -90,7 +102,7 @@ export function initFactory(): Function {
let installAction = null;
const installedVersion = await storageService.get<string>(ConstantsService.installedVersionKey);
const currentVersion = platformUtilsService.getApplicationVersion();
const currentVersion = await platformUtilsService.getApplicationVersion();
if (installedVersion == null) {
installAction = 'install';
} else if (installedVersion !== currentVersion) {
@@ -127,6 +139,7 @@ export function initFactory(): Function {
{ provide: PlatformUtilsServiceAbstraction, useValue: platformUtilsService },
{ provide: ApiServiceAbstraction, useValue: apiService },
{ provide: UserServiceAbstraction, useValue: userService },
{ provide: ApiKeyServiceAbstraction, useValue: apiKeyService },
{ provide: MessagingServiceAbstraction, useValue: messagingService },
{ provide: BroadcasterService, useValue: broadcasterService },
{ provide: StorageServiceAbstraction, useValue: storageService },
@@ -134,6 +147,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,38 +14,44 @@
<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">
<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}}
</button>
<form #startForm [appApiAction]="startPromise" class="d-inline">
<button (click)="start()" class="btn btn-primary"
[disabled]="startForm.loading">
<i class="fa fa-play fa-fw" [hidden]="startForm.loading"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startForm.loading"></i>
{{'startSync' | i18n}}
</button>
</form>
<button (click)="stop()" class="btn btn-primary">
<i class="fa fa-stop fa-fw"></i>
{{'stopSync' | i18n}}
</button>
<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>
<form #syncForm [appApiAction]="syncPromise" class="d-inline">
<button (click)="sync()" class="btn btn-primary"
[disabled]="syncForm.loading">
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncForm.loading}"></i>
{{'syncNow' | i18n}}
</button>
</form>
</div>
</div>
<div class="card">
<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">
<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}}
</button>
<form #simForm [appApiAction]="simPromise" class="d-inline">
<button (click)="simulate()" class="btn btn-primary"
[disabled]="simForm.loading">
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simForm.loading"></i>
<i class="fa fa-bug fa-fw" [hidden]="simForm.loading"></i>
{{'testNow' | i18n}}
</button>
</form>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="simSinceLast" [(ngModel)]="simSinceLast">
<label class="form-check-label" for="simSinceLast">{{'testLastSync' | i18n}}</label>
</div>
<ng-container *ngIf="!simBtn.loading && (simUsers || simGroups)">
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
<hr />
<div class="row">
<div class="col-lg">

View File

@@ -8,9 +8,9 @@ import {
import { ToasterService } from 'angular2-toaster';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { StateService } from 'jslib/abstractions/state.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { SyncService } from '../../services/sync.service';
@@ -19,7 +19,7 @@ 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 { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
import { ConnectorUtils } from '../../utils';

View File

@@ -8,11 +8,11 @@ import {
import { ToasterService } from 'angular2-toaster';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ConfigurationService } from '../../services/configuration.service';
@@ -32,7 +32,7 @@ export class MoreComponent implements OnInit {
private toasterService: ToasterService, private broadcasterService: BroadcasterService,
private ngZone: NgZone, private changeDetectorRef: ChangeDetectorRef) { }
ngOnInit() {
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
@@ -51,7 +51,7 @@ export class MoreComponent implements OnInit {
});
this.year = new Date().getFullYear().toString();
this.version = this.platformUtilsService.getApplicationVersion();
this.version = await this.platformUtilsService.getApplicationVersion();
}
ngOnDestroy() {

View File

@@ -37,8 +37,7 @@
<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>
<label class="form-check-label" for="pagedSearch">{{'ldapPagedResults' | i18n}}</label>
</div>
</div>
<div class="form-group">
@@ -99,8 +98,8 @@
<div class="form-check">
<input class="form-check-input" type="checkbox" id="certDoNotVerify"
[(ngModel)]="ldap.sslAllowUnauthorized" name="CertDoNoVerify">
<label class="form-check-label"
for="certDoNotVerify">{{'ldapCertDoNotVerify' | i18n}}</label>
<label class="form-check-label" for="certDoNotVerify">{{'ldapCertDoNotVerify' |
i18n}}</label>
</div>
</div>
</div>
@@ -214,17 +213,6 @@
</div>
</div>
<div class="card mb-3">
<h3 class="card-header">{{'account' | i18n}}</h3>
<div class="card-body">
<div class="form-group">
<label for="organizationId">{{'organization' | i18n}}</label>
<select class="form-control" id="organizationId" name="OrganizationId" [(ngModel)]="organizationId">
<option *ngFor="let o of organizationOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div>
</div>
</div>
</div>
<div class="col-sm">
<div class="card">
@@ -250,6 +238,13 @@
<label class="form-check-label" for="overwriteExisting">{{'overwriteExisting' | i18n}}</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="largeImport" [(ngModel)]="sync.largeImport"
name="LargeImport">
<label class="form-check-label" for="largeImport">{{'largeImport' | i18n}}</label>
</div>
</div>
<div [hidden]="directory != directoryType.Ldap">
<div [hidden]="ldap.ad">
<div class="form-group">
@@ -271,7 +266,8 @@
<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"
@@ -281,7 +277,7 @@
</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">
@@ -344,12 +340,14 @@
<div class="form-check">
<input class="form-check-input" type="checkbox" id="syncGroups" [(ngModel)]="sync.groups"
name="SyncGroups">
<label class="form-check-label" for="syncGroups">{{'syncGroups' | i18n}}</label>
<label class="form-check-label" for="syncGroups">{{(directory !== directoryType.OneLogin ?
'syncGroups' : 'syncGroupsOneLogin') | i18n}}</label>
</div>
</div>
<div [hidden]="!sync.groups">
<div class="form-group">
<label for="groupFilter">{{'groupFilter' | i18n}}</label>
<label for="groupFilter">{{(directory !== directoryType.OneLogin ? 'groupFilter' :
'groupFilterOneLogin') | 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}}

View File

@@ -6,10 +6,10 @@ import {
OnInit,
} from '@angular/core';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { StateService } from 'jslib/abstractions/state.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { ProfileOrganizationResponse } from 'jslib/models/response/profileOrganizationResponse';
import { ProfileOrganizationResponse } from 'jslib-common/models/response/profileOrganizationResponse';
import { ConfigurationService } from '../../services/configuration.service';
@@ -37,9 +37,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
okta = new OktaConfiguration();
oneLogin = new OneLoginConfiguration();
sync = new SyncConfiguration();
organizationId: string;
directoryOptions: any[];
organizationOptions: any[];
constructor(private i18nService: I18nService, private configurationService: ConfigurationService,
private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone,
@@ -55,15 +53,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
}
async ngOnInit() {
this.organizationOptions = [{ name: this.i18nService.t('select'), value: null }];
const orgs = await this.stateService.get<ProfileOrganizationResponse[]>('profileOrganizations');
if (orgs != null) {
for (const org of orgs) {
this.organizationOptions.push({ name: org.name, value: org.id });
}
}
this.organizationId = await this.configurationService.getOrganizationId();
this.directory = await this.configurationService.getDirectoryType();
this.ldap = (await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) ||
this.ldap;
@@ -87,7 +76,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
if (this.ldap != null && this.ldap.ad) {
this.ldap.pagedSearch = true;
}
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);
@@ -105,7 +93,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);

View File

@@ -1,33 +1,36 @@
import * as fs from 'fs';
import * as path from 'path';
import { LogLevelType } from 'jslib/enums/logLevelType';
import { LogLevelType } from 'jslib-common/enums/logLevelType';
import { AuthService } from 'jslib/services/auth.service';
import { AuthService } from './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 { NodeApiService } from './services/nodeApi.service';
import { SyncService } from './services/sync.service';
import { CliPlatformUtilsService } from 'jslib/cli/services/cliPlatformUtils.service';
import { ConsoleLogService } from 'jslib/cli/services/consoleLog.service';
import { CliPlatformUtilsService } from 'jslib-node/cli/services/cliPlatformUtils.service';
import { ConsoleLogService } from 'jslib-node/cli/services/consoleLog.service';
import { NodeCryptoFunctionService } from 'jslib-node/services/nodeCryptoFunction.service';
import { 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 { LowdbStorageService } from 'jslib/services/lowdbStorage.service';
import { NodeApiService } from 'jslib/services/nodeApi.service';
import { NodeCryptoFunctionService } from 'jslib/services/nodeCryptoFunction.service';
import { NoopMessagingService } from 'jslib/services/noopMessaging.service';
import { TokenService } from 'jslib/services/token.service';
import { UserService } from 'jslib/services/user.service';
import { ApiKeyService } from 'jslib-common/services/apiKey.service';
import { AppIdService } from 'jslib-common/services/appId.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
import { ContainerService } from 'jslib-common/services/container.service';
import { CryptoService } from 'jslib-common/services/crypto.service';
import { EnvironmentService } from 'jslib-common/services/environment.service';
import { NoopMessagingService } from 'jslib-common/services/noopMessaging.service';
import { PasswordGenerationService } from 'jslib-common/services/passwordGeneration.service';
import { TokenService } from 'jslib-common/services/token.service';
import { UserService } from 'jslib-common/services/user.service';
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
import { StorageService as StorageServiceAbstraction } from 'jslib-common/abstractions/storage.service';
import { Program } from './program';
import { refreshToken } from './services/api.service';
// tslint:disable-next-line
const packageJson = require('./package.json');
@@ -46,12 +49,14 @@ export class Main {
appIdService: AppIdService;
apiService: NodeApiService;
environmentService: EnvironmentService;
apiKeyService: ApiKeyService;
userService: UserService;
containerService: ContainerService;
cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService;
configurationService: ConfigurationService;
syncService: SyncService;
passwordGenerationService: PasswordGenerationService;
program: Program;
constructor() {
@@ -76,27 +81,30 @@ export class Main {
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);
level => process.env.BITWARDENCLI_CONNECTOR_DEBUG !== 'true' && level <= LogLevelType.Info);
this.cryptoFunctionService = new NodeCryptoFunctionService();
this.storageService = new LowdbStorageService(null, this.dataFilePath, true);
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.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,
this.apiService = new NodeApiService(this.tokenService, this.platformUtilsService, this.refreshTokenCallback,
async (expired: boolean) => await this.logout());
this.environmentService = new EnvironmentService(this.apiService, this.storageService, null);
this.apiKeyService = new ApiKeyService(this.tokenService, this.storageService);
this.userService = new UserService(this.tokenService, this.storageService);
this.containerService = new ContainerService(this.cryptoService);
this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, this.tokenService,
this.appIdService, this.i18nService, this.platformUtilsService, this.messagingService, true);
this.appIdService, this.i18nService, this.platformUtilsService, this.messagingService, null,
this.logService, this.apiKeyService, false);
this.configurationService = new ConfigurationService(this.storageService, this.secureStorageService,
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== 'true');
this.syncService = new SyncService(this.configurationService, this.logService, this.cryptoFunctionService,
this.apiService, this.messagingService, this.i18nService);
this.passwordGenerationService = new PasswordGenerationService(this.cryptoService, this.storageService, null);
this.program = new Program(this);
}
@@ -106,14 +114,16 @@ export class Main {
}
async logout() {
await Promise.all([
this.tokenService.clearToken(),
this.userService.clear(),
]);
await this.tokenService.clearToken();
await this.apiKeyService.clear();
}
refreshTokenCallback() {
return refreshToken(this.apiKeyService, this.authService);
}
private async init() {
this.storageService.init();
await this.storageService.init();
this.containerService.attachToWindow(global);
await this.environmentService.setUrlsFromStorage();
// Dev Server URLs. Comment out the line above.
@@ -127,7 +137,7 @@ export class Main {
this.authService.init();
const installedVersion = await this.storageService.get<string>(ConstantsService.installedVersionKey);
const currentVersion = this.platformUtilsService.getApplicationVersion();
const currentVersion = await this.platformUtilsService.getApplicationVersion();
if (installedVersion == null || installedVersion !== currentVersion) {
await this.storageService.save(ConstantsService.installedVersionKey, currentVersion);
}

View File

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

View File

@@ -1,14 +1,14 @@
import * as program from 'commander';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/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 { Response } from 'jslib-node/cli/models/response';
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse';
import { AzureConfiguration } from '../models/azureConfiguration';
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
@@ -19,6 +19,8 @@ import { SyncConfiguration } from '../models/syncConfiguration';
import { ConnectorUtils } from '../utils';
import { NodeUtils } from 'jslib-common/misc/nodeUtils';
export class ConfigCommand {
private directory: DirectoryType;
private ldap = new LdapConfiguration();
@@ -31,8 +33,15 @@ export class ConfigCommand {
constructor(private environmentService: EnvironmentService, private i18nService: I18nService,
private configurationService: ConfigurationService) { }
async run(setting: string, value: string, cmd: program.Command): Promise<Response> {
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':

View File

@@ -2,13 +2,13 @@ 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';
import { Response } from 'jslib-node/cli/models/response';
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse';
export class LastSyncCommand {
constructor(private configurationService: ConfigurationService) { }
async run(object: string, cmd: program.Command): Promise<Response> {
async run(object: string): Promise<Response> {
try {
switch (object.toLowerCase()) {
case 'groups':

View File

@@ -1,16 +1,14 @@
import * as program from 'commander';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { SyncService } from '../services/sync.service';
import { Response } from 'jslib/cli/models/response';
import { MessageResponse } from 'jslib/cli/models/response/messageResponse';
import { Response } from 'jslib-node/cli/models/response';
import { MessageResponse } from 'jslib-node/cli/models/response/messageResponse';
export class SyncCommand {
constructor(private syncService: SyncService, private i18nService: I18nService) { }
async run(cmd: program.Command): Promise<Response> {
async run(): Promise<Response> {
try {
const result = await this.syncService.sync(false, false);
const groupCount = result[0] != null ? result[0].length : 0;

View File

@@ -1,18 +1,18 @@
import * as program from 'commander';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { SyncService } from '../services/sync.service';
import { ConnectorUtils } from '../utils';
import { Response } from 'jslib/cli/models/response';
import { Response } from 'jslib-node/cli/models/response';
import { TestResponse } from '../models/response/testResponse';
export class TestCommand {
constructor(private syncService: SyncService, private i18nService: I18nService) { }
async run(cmd: program.Command): Promise<Response> {
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);

View File

@@ -20,12 +20,30 @@
"emailRequired": {
"message": "Email address is required."
},
"clientIdRequired": {
"message": "Client Id is required."
},
"invalidClientId": {
"message": "Invalid Client Id provided."
},
"clientSecretRequired": {
"message": "Client Secret is required."
},
"orgApiKeyRequired": {
"message": "Api Key must belong to an Organization"
},
"failedToSaveCredentials": {
"message": "Failed to save credentials"
},
"invalidEmail": {
"message": "Invalid email address."
},
"masterPassRequired": {
"message": "Master password is required."
},
"missingRequiredInput": {
"message": "Missing required input."
},
"unexpectedError": {
"message": "An unexpected error has occurred."
},
@@ -140,6 +158,9 @@
"baseUrl": {
"message": "Server URL"
},
"webVaultUrl": {
"message": "Web Vault Server URL"
},
"apiUrl": {
"message": "API Server URL"
},
@@ -399,6 +420,12 @@
"groupFilter": {
"message": "Group Filter"
},
"syncGroupsOneLogin": {
"message": "Sync roles"
},
"groupFilterOneLogin": {
"message": "Role Filter"
},
"groupObjectClass": {
"message": "Group Object Class"
},
@@ -414,6 +441,19 @@
"sync": {
"message": "Sync"
},
"duplicateEmails": {
"message": "Emails must be unique. Multiple entries pulled with the following emails:",
"desription": "Error message displayed when duplicate email addresses are synced. Followed by a list of duplicate emails."
},
"andMore": {
"message": "and $NUMBER$ more...",
"placeholders": {
"NUMBER": {
"content": "$1",
"example": "10"
}
}
},
"ldapEncrypted": {
"message": "This server uses an encrypted connection"
},
@@ -566,7 +606,7 @@
"message": "Welcome to the Bitwarden Directory Connector"
},
"logInDesc": {
"message": "Log in as an organization admin user below."
"message": "Log in with an organization API key below."
},
"dirConfigIncomplete": {
"message": "Directory configuration incomplete."
@@ -600,6 +640,9 @@
}
}
},
"largeImport": {
"message": "More than 2000 users or groups are expected to sync."
},
"overwriteExisting": {
"message": "Overwrite existing organization users based on current sync settings."
},
@@ -611,5 +654,113 @@
},
"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,13 +5,13 @@ import { MenuMain } from './main/menu.main';
import { MessagingMain } from './main/messaging.main';
import { I18nService } from './services/i18n.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';
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';
export class Main {
logService: ElectronLogService;
@@ -41,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
@@ -52,7 +52,7 @@ export class Main {
this.i18nService = new I18nService('en', './locales/');
this.storageService = new ElectronStorageService(app.getPath('userData'));
this.windowMain = new WindowMain(this.storageService, false, 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');
@@ -63,11 +63,11 @@ export class Main {
}, '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);
});
this.keytarStorageListener = new KeytarStorageListener('Bitwarden Directory Connector');
this.keytarStorageListener = new KeytarStorageListener('Bitwarden Directory Connector', null);
}
bootstrap() {
@@ -78,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

@@ -6,7 +6,7 @@ import {
import { Main } from '../main';
import { BaseMenu } from 'jslib/electron/baseMenu';
import { BaseMenu } from 'jslib-electron/baseMenu';
export class MenuMain extends BaseMenu {
menu: Menu;

View File

@@ -3,9 +3,9 @@ import {
ipcMain,
} from 'electron';
import { TrayMain } from 'jslib/electron/tray.main';
import { UpdaterMain } from 'jslib/electron/updater.main';
import { WindowMain } from 'jslib/electron/window.main';
import { TrayMain } from 'jslib-electron/tray.main';
import { UpdaterMain } from 'jslib-electron/updater.main';
import { WindowMain } from 'jslib-electron/window.main';
import { MenuMain } from './menu.main';

View File

@@ -3,7 +3,7 @@ import { UserResponse } from './userResponse';
import { SimResult } from '../simResult';
import { BaseResponse } from 'jslib/cli/models/response/baseResponse';
import { BaseResponse } from 'jslib-node/cli/models/response/baseResponse';
export class TestResponse implements BaseResponse {
object: string;
@@ -14,9 +14,9 @@ export class TestResponse implements BaseResponse {
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)) : [];
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

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

View File

@@ -2,7 +2,7 @@
"name": "bitwarden-directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2.7.0",
"version": "2.9.3",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
@@ -12,9 +12,10 @@
"url": "https://github.com/bitwarden/directory-connector"
},
"dependencies": {
"electron-log": "2.2.17",
"electron-store": "1.3.0",
"electron-updater": "4.2.0",
"keytar": "4.13.0"
"browser-hrtime": "^1.1.8",
"electron-log": "4.3.5",
"electron-store": "8.0.0",
"electron-updater": "4.3.9",
"keytar": "7.6.0"
}
}

View File

@@ -1,4 +1,4 @@
import * as chk from 'chalk';
import * as chalk from 'chalk';
import * as program from 'commander';
import * as path from 'path';
@@ -10,16 +10,18 @@ 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 { LoginCommand } from 'jslib-node/cli/commands/login.command';
import { LogoutCommand } from 'jslib-node/cli/commands/logout.command';
import { UpdateCommand } from 'jslib-node/cli/commands/update.command';
import { BaseProgram } from 'jslib/cli/baseProgram';
import { BaseProgram } from 'jslib-node/cli/baseProgram';
import { Response } from 'jslib/cli/models/response';
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { Response } from 'jslib-node/cli/models/response';
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse';
import { Utils } from 'jslib-common/misc/utils';
const 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') {
@@ -30,18 +32,22 @@ const writeLn = (s: string, finalLine: boolean = false, error: boolean = false)
};
export class Program extends BaseProgram {
private apiKeyService: ApiKeyService;
constructor(private main: Main) {
super(main.userService, writeLn);
this.apiKeyService = main.apiKeyService;
}
run() {
async run() {
program
.option('--pretty', 'Format output. JSON is tabbed with two spaces.')
.option('--raw', 'Return raw output instead of a descriptive message.')
.option('--response', 'Return a JSON formatted version of response output.')
.option('--cleanexit', 'Exit with a success exit code (0) unless an error is thrown.')
.option('--quiet', 'Don\'t return anything to stdout.')
.option('--nointeraction', 'Do not prompt for interactive user input.')
.version(this.main.platformUtilsService.getApplicationVersion(), '-v, --version');
.version(await this.main.platformUtilsService.getApplicationVersion(), '-v, --version');
program.on('option:pretty', () => {
process.env.BW_PRETTY = 'true';
@@ -59,6 +65,10 @@ export class Program extends BaseProgram {
process.env.BW_RESPONSE = 'true';
});
program.on('option:cleanexit', () => {
process.env.BW_CLEANEXIT = 'true';
});
program.on('option:nointeraction', () => {
process.env.BW_NOINTERACTION = 'true';
});
@@ -82,26 +92,26 @@ export class Program extends BaseProgram {
});
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.')
.on('--help', () => {
writeLn('\n Notes:');
writeLn('');
writeLn(' See docs for valid `method` enum values.');
writeLn('');
writeLn(' Examples:');
writeLn('');
writeLn(' bw login');
writeLn(' bw login john@example.com myPassword321');
writeLn(' bw login john@example.com myPassword321 --method 1 --code 249213');
writeLn('', true);
.command('login [clientId] [clientSecret]')
.description('Log into an organization account.', {
clientId: 'Client_id part of your organization\'s API key',
clientSecret: 'Client_secret part of your organization\'s API key',
})
.action(async (email: string, password: string, cmd: program.Command) => {
.action(async (clientId: string, clientSecret: string, options: program.OptionValues) => {
await this.exitIfAuthed();
const command = new LoginCommand(this.main.authService, this.main.apiService, this.main.i18nService);
const response = await command.run(email, password, cmd);
const command = new LoginCommand(this.main.authService, this.main.apiService, this.main.i18nService,
this.main.environmentService, this.main.passwordGenerationService, this.main.cryptoFunctionService,
this.main.platformUtilsService, 'connector');
if (!Utils.isNullOrWhitespace(clientId)) {
process.env.BW_CLIENTID = clientId;
}
if (!Utils.isNullOrWhitespace(clientSecret)) {
process.env.BW_CLIENTSECRET = clientSecret;
}
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
const response = await command.run(null, null, options);
this.processResponse(response);
});
@@ -111,14 +121,14 @@ export class Program extends BaseProgram {
.on('--help', () => {
writeLn('\n Examples:');
writeLn('');
writeLn(' bw logout');
writeLn(' bwdc logout');
writeLn('', true);
})
.action(async (cmd) => {
.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(cmd);
const response = await command.run();
this.processResponse(response);
});
@@ -133,10 +143,10 @@ export class Program extends BaseProgram {
writeLn(' bwdc test --last');
writeLn('', true);
})
.action(async (cmd) => {
.action(async (options: program.OptionValues) => {
await this.exitIfNotAuthed();
const command = new TestCommand(this.main.syncService, this.main.i18nService);
const response = await command.run(cmd);
const response = await command.run(options);
this.processResponse(response);
});
@@ -149,10 +159,10 @@ export class Program extends BaseProgram {
writeLn(' bwdc sync');
writeLn('', true);
})
.action(async (cmd) => {
.action(async () => {
await this.exitIfNotAuthed();
const command = new SyncCommand(this.main.syncService, this.main.i18nService);
const response = await command.run(cmd);
const response = await command.run();
this.processResponse(response);
});
@@ -170,16 +180,18 @@ export class Program extends BaseProgram {
writeLn(' bwdc last-sync users');
writeLn('', true);
})
.action(async (object: string, cmd: program.Command) => {
.action(async (object: string) => {
await this.exitIfNotAuthed();
const command = new LastSyncCommand(this.main.configurationService);
const response = await command.run(object, cmd);
const response = await command.run(object);
this.processResponse(response);
});
program
.command('config <setting> <value>')
.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('');
@@ -197,16 +209,17 @@ export class Program extends BaseProgram {
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, value, cmd) => {
.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, cmd);
const response = await command.run(setting, value, options);
this.processResponse(response);
});
@@ -233,9 +246,9 @@ export class Program extends BaseProgram {
writeLn(' bwdc clear-cache');
writeLn('', true);
})
.action(async (cmd) => {
.action(async (options: program.OptionValues) => {
const command = new ClearCacheCommand(this.main.configurationService, this.main.i18nService);
const response = await command.run(cmd);
const response = await command.run(options);
this.processResponse(response);
});
@@ -255,10 +268,10 @@ export class Program extends BaseProgram {
writeLn(' bwdc update --raw');
writeLn('', true);
})
.action(async (cmd) => {
.action(async () => {
const command = new UpdateCommand(this.main.platformUtilsService, this.main.i18nService,
'directory-connector', 'bwdc', false);
const response = await command.run(cmd);
const response = await command.run();
this.processResponse(response);
});
@@ -269,4 +282,20 @@ export class Program extends BaseProgram {
program.outputHelp();
}
}
async exitIfAuthed() {
const authed = await this.apiKeyService.isAuthenticated();
if (authed) {
const type = await this.apiKeyService.getEntityType();
const id = await this.apiKeyService.getEntityId();
this.processResponse(Response.error('You are already logged in as ' + type + '.' + id + '.'), true);
}
}
async exitIfNotAuthed() {
const authed = await this.apiKeyService.isAuthenticated();
if (!authed) {
this.processResponse(Response.error('You are not logged in.'), true);
}
}
}

View File

@@ -1,4 +1,4 @@
$theme-colors: ( "primary": #175DDC, "primary-accent": #1252A3, "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

@@ -0,0 +1,30 @@
import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { TokenService } from 'jslib-common/abstractions/token.service';
import { ApiService as ApiServiceBase } from 'jslib-common/services/api.service';
export async function refreshToken(apiKeyService: ApiKeyService, authService: AuthService) {
try {
const clientId = await apiKeyService.getClientId();
const clientSecret = await apiKeyService.getClientSecret();
if (clientId != null && clientSecret != null) {
await authService.logInApiKey(clientId, clientSecret);
}
} catch (e) {
return Promise.reject(e);
}
}
export class ApiService extends ApiServiceBase {
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService,
private refreshTokenCallback: () => Promise<void>, logoutCallback: (expired: boolean) => Promise<void>,
customUserAgent: string = null) {
super(tokenService, platformUtilsService, logoutCallback, customUserAgent);
}
doRefreshToken(): Promise<void> {
return this.refreshTokenCallback();
}
}

View File

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

View File

@@ -12,10 +12,10 @@ 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';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
const NextLink = '@odata.nextLink';
const DeltaLink = '@odata.deltaLink';
@@ -29,7 +29,7 @@ enum UserSetType {
ExcludeGroup,
}
export class AzureDirectoryService extends BaseDirectoryService implements DirectoryService {
export class AzureDirectoryService extends BaseDirectoryService implements IDirectoryService {
private client: graph.Client;
private dirConfig: AzureConfiguration;
private syncConfig: SyncConfiguration;
@@ -68,9 +68,9 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
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];
@@ -90,7 +90,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
continue;
}
const entry = this.buildUser(user);
if (await this.filterOutUserResult(setFilter, entry)) {
if (await this.filterOutUserResult(setFilter, entry, true)) {
continue;
}
@@ -147,7 +147,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
if (!entry.disabled && !entry.deleted) {
continue;
}
if (await this.filterOutUserResult(setFilter, entry)) {
if (await this.filterOutUserResult(setFilter, entry, false)) {
continue;
}
@@ -170,6 +170,55 @@ 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;
@@ -208,7 +257,8 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
return [userSetType, set];
}
private async filterOutUserResult(setFilter: [UserSetType, Set<string>], user: UserEntry): Promise<boolean> {
private async filterOutUserResult(setFilter: [UserSetType, Set<string>], user: UserEntry,
checkGroupsFilter: boolean): Promise<boolean> {
if (setFilter == null) {
return false;
}
@@ -224,20 +274,22 @@ export class AzureDirectoryService extends BaseDirectoryService implements Direc
return this.filterOutResult([userSetTypeExclude, setFilter[1]], user.email);
}
try {
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;
}
} catch { }
// 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;
}
@@ -331,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);
@@ -361,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);
@@ -374,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

@@ -1,12 +1,12 @@
import { DirectoryType } from '../enums/directoryType';
import { StorageService } from 'jslib/abstractions/storage.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { AzureConfiguration } from '../models/azureConfiguration';
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
import { LdapConfiguration } from '../models/ldapConfiguration';
import { OktaConfiguration } from '../models/oktaConfiguration';
import { OneLoginConfiguration } from '../models/oneLoginConfiguration';
import { SyncConfiguration } from '../models/syncConfiguration';
import { OneLoginConfiguration } from 'src/models/oneLoginConfiguration';
const StoredSecurely = '[STORED SECURELY]';
const Keys = {

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';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/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];
@@ -140,7 +140,7 @@ 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;
@@ -156,7 +156,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
if (res.data.groups != null) {
for (const group of res.data.groups) {
if (!this.filterOutResult(setFilter, group.name)) {
const entry = await this.buildGroup(group);
const entry = await this.buildGroup(group, users);
entries.push(entry);
}
}
@@ -170,7 +170,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
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();
@@ -192,18 +192,18 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements Dire
if (member.type == null) {
continue;
}
if (member.role == null || member.role.toLowerCase() !== 'member') {
continue;
}
if (member.status == null || member.status.toLowerCase() !== 'active') {
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);
}
}
}
}

View File

@@ -1,7 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { I18nService as BaseI18nService } from 'jslib/services/i18n.service';
import { I18nService as BaseI18nService } from 'jslib-common/services/i18n.service';
export class I18nService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) {

View File

@@ -4,17 +4,21 @@ import {
setPassword,
} from 'keytar';
import { StorageService } from 'jslib/abstractions/storage.service';
import { StorageService } from 'jslib-common/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 getPassword(this.serviceName, key).then(val => {
return JSON.parse(val) as T;
});
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) != null;
}
save(key: string, obj: any): Promise<any> {
return setPassword(this.serviceName, key, JSON.stringify(obj));
}

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,16 +11,16 @@ 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';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { Utils } from 'jslib/misc/utils';
import { Utils } from 'jslib-common/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,7 +233,7 @@ export class LdapDirectoryService implements DirectoryService {
}
private makeSearchPath(pathPrefix: string) {
if (this.dirConfig.rootPath.indexOf('dc=') === -1) {
if (this.dirConfig.rootPath.toLowerCase().indexOf('dc=') === -1) {
return pathPrefix;
}
if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== '') {
@@ -303,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);
});
});
@@ -322,7 +324,7 @@ export class LdapDirectoryService implements DirectoryService {
}
private async bind(): Promise<any> {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
reject(this.i18nService.t('dirConfigIncomplete'));
return;
@@ -360,9 +362,8 @@ export class LdapDirectoryService implements DirectoryService {
}
}
if (Object.keys(tlsOptions).length > 0) {
options.tlsOptions = tlsOptions;
}
tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
options.tlsOptions = tlsOptions;
this.client = ldap.createClient(options);
@@ -381,7 +382,7 @@ export class LdapDirectoryService implements DirectoryService {
if (err != null) {
reject(err.message);
} else {
this.client.bind(user, pass, (err2) => {
this.client.bind(user, pass, err2 => {
if (err2 != null) {
reject(err2.message);
} else {
@@ -391,7 +392,7 @@ export class LdapDirectoryService implements DirectoryService {
}
});
} else {
this.client.bind(user, pass, (err) => {
this.client.bind(user, pass, err => {
if (err != null) {
reject(err.message);
} else {
@@ -402,9 +403,9 @@ export class LdapDirectoryService implements DirectoryService {
});
}
private async unbind(): Promise<any> {
private async unbind(): Promise<void> {
return new Promise((resolve, reject) => {
this.client.unbind((err) => {
this.client.unbind(err => {
if (err != null) {
reject(err);
} else {
@@ -425,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,28 @@
import * as lock from 'proper-lockfile';
import { LogService } from 'jslib-common/abstractions/log.service';
import { LowdbStorageService as LowdbStorageServiceBase } from 'jslib-node/services/lowdbStorage.service';
import { Utils } from 'jslib-common/misc/utils';
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

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

View File

@@ -7,18 +7,19 @@ 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';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/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 {
const DelayBetweenBuildGroupCallsInMilliseconds = 500;
export class OktaDirectoryService extends BaseDirectoryService implements IDirectoryService {
private dirConfig: OktaConfiguration;
private syncConfig: SyncConfiguration;
private client: any;
private lastBuildGroupCall: number;
constructor(private configurationService: ConfigurationService, private logService: LogService,
private i18nService: I18nService) {
@@ -45,11 +46,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 +55,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 +68,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 +85,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 +118,12 @@ 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.filter(g => !this.filterOutResult(setFilter, g.profile.name))) {
const entry = await this.buildGroup(group);
if (entry != null) {
entries.push(entry);
}
}
});
return entries;
@@ -131,8 +135,18 @@ 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);
// throttle some to avoid rate limiting
const neededDelay = DelayBetweenBuildGroupCallsInMilliseconds - (Date.now() - this.lastBuildGroupCall);
if (neededDelay > 0) {
await new Promise(resolve =>
setTimeout(resolve, neededDelay));
}
this.lastBuildGroupCall = Date.now();
await this.apiGetMany('groups/' + group.id + '/users').then((users: any[]) => {
for (const user of users) {
entry.userMemberExternalIds.add(user.id);
}
});
return entry;
@@ -152,4 +166,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

@@ -7,12 +7,15 @@ 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';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
export class OneLoginDirectoryService extends BaseDirectoryService implements DirectoryService {
// 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;
@@ -57,7 +60,7 @@ export class OneLoginDirectoryService extends BaseDirectoryService implements Di
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];
@@ -69,7 +72,7 @@ export class OneLoginDirectoryService extends BaseDirectoryService implements Di
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) => {
this.allUsers.forEach(user => {
const entry = this.buildUser(user);
if (entry != null && !this.filterOutResult(setFilter, entry.email)) {
entries.push(entry);
@@ -82,9 +85,22 @@ export class OneLoginDirectoryService extends BaseDirectoryService implements Di
const entry = new UserEntry();
entry.externalId = user.id;
entry.referenceId = user.id;
entry.email = user.email != null ? user.email.trim().toLowerCase() : null;
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;
}
@@ -93,7 +109,7 @@ export class OneLoginDirectoryService extends BaseDirectoryService implements Di
const query = this.createDirectoryQuery(this.syncConfig.groupFilter);
this.logService.info('Querying groups.');
const roles = await this.apiGetMany('roles' + (query != null ? '?' + query : ''));
roles.forEach((role) => {
roles.forEach(role => {
const entry = this.buildGroup(role);
if (entry != null && !this.filterOutResult(setFilter, entry.name)) {
entries.push(entry);
@@ -109,7 +125,7 @@ export class OneLoginDirectoryService extends BaseDirectoryService implements Di
entry.name = group.name;
if (this.allUsers != null) {
this.allUsers.forEach((user) => {
this.allUsers.forEach(user => {
if (user.role_id != null && user.role_id.indexOf(entry.referenceId) > -1) {
entry.userMemberExternalIds.add(user.id);
}
@@ -144,12 +160,11 @@ export class OneLoginDirectoryService extends BaseDirectoryService implements Di
const req: RequestInit = {
method: 'GET',
headers: new Headers({
'Authorization': 'bearer:' + this.accessToken,
'Accept': 'application/json',
Authorization: 'bearer:' + this.accessToken,
Accept: 'application/json',
}),
};
const response = await fetch(
new Request(url, req));
const response = await fetch(new Request(url, req));
if (response.status === 200) {
const responseJson = await response.json();
return responseJson;
@@ -173,4 +188,8 @@ export class OneLoginDirectoryService extends BaseDirectoryService implements Di
}
return this.apiGetMany(response.pagination.next_link, currentData);
}
private validEmailAddress(email: string) {
return email != null && email !== '' && ValidEmailRegex.test(email);
}
}

View File

@@ -4,21 +4,19 @@ import { GroupEntry } from '../models/groupEntry';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
import { ImportDirectoryRequest } from 'jslib/models/request/importDirectoryRequest';
import { ImportDirectoryRequestGroup } from 'jslib/models/request/importDirectoryRequestGroup';
import { ImportDirectoryRequestUser } from 'jslib/models/request/importDirectoryRequestUser';
import { OrganizationImportRequest } from 'jslib-common/models/request/organizationImportRequest';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { LogService } from 'jslib/abstractions/log.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { Utils } from 'jslib/misc/utils';
import { Utils } from 'jslib-common/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';
@@ -57,7 +55,16 @@ export class SyncService {
this.flattenUsersToGroups(groups, groups);
}
if (test || ((groups == null || groups.length === 0) && (users == null || users.length === 0))) {
const duplicateEmails = this.findDuplicateUserEmails(users);
if (duplicateEmails.length > 0) {
const emailsMessage = duplicateEmails.length < 4 ?
duplicateEmails.join('\n') :
duplicateEmails.slice(0, 3).join('\n') + '\n' + this.i18nService.t('andMore', `${duplicateEmails.length - 3}`);
throw new Error(this.i18nService.t('duplicateEmails') + '\n' + emailsMessage);
}
if (test || (!syncConfig.overwriteExisting &&
(groups == null || groups.length === 0) && (users == null || users.length === 0))) {
if (!test) {
await this.saveSyncTimes(syncConfig, now);
}
@@ -66,23 +73,29 @@ export class SyncService {
return [groups, users];
}
const req = this.buildRequest(groups, users, syncConfig.removeDisabled, syncConfig.overwriteExisting);
const req = this.buildRequest(groups, users, syncConfig.removeDisabled, syncConfig.overwriteExisting, syncConfig.largeImport);
const reqJson = JSON.stringify(req);
const orgId = await this.configurationService.getOrganizationId();
if (orgId == null) {
throw new Error('Organization not set.');
}
// TODO: Remove hashLegacy once we're sure clients have had time to sync new hashes
let hashLegacy: string = null;
const hashBuffLegacy = await this.cryptoFunctionService.hash(this.apiService.apiBaseUrl + reqJson, 'sha256');
if (hashBuffLegacy != null) {
hashLegacy = Utils.fromBufferToB64(hashBuffLegacy);
}
let hash: string = null;
const hashBuf = await this.cryptoFunctionService.hash(this.apiService.apiBaseUrl + reqJson, 'sha256');
if (hashBuf != null) {
hash = Utils.fromBufferToB64(hashBuf);
const hashBuff = await this.cryptoFunctionService.hash(this.apiService.apiBaseUrl + orgId + reqJson, 'sha256');
if (hashBuff != null) {
hash = Utils.fromBufferToB64(hashBuff);
}
const lastHash = await this.configurationService.getLastSyncHash();
if (lastHash == null || hash !== lastHash) {
const orgId = await this.configurationService.getOrganizationId();
if (orgId == null) {
throw new Error('Organization not set.');
}
await this.apiService.postImportDirectory(orgId, req);
if (lastHash == null || (hash !== lastHash && hashLegacy !== lastHash)) {
await this.apiService.postPublicImportDirectory(req);
await this.configurationService.saveLastSyncHash(hash);
} else {
groups = null;
@@ -103,8 +116,21 @@ export class SyncService {
}
}
private findDuplicateUserEmails(users: UserEntry[]) {
const duplicatedEmails = new Array<string>();
users.reduce((agg, user) => {
if (agg.includes(user.email) && !duplicatedEmails.includes(user.email)) {
duplicatedEmails.push(user.email);
} else {
agg.push(user.email);
}
return agg;
}, new Array<string>());
return duplicatedEmails;
}
private filterUnsupportedUsers(users: UserEntry[]): UserEntry[] {
return users == null ? null : users.filter((u) => u.email == null || u.email.length <= 50);
return users == null ? null : users.filter(u => u.email?.length <= 256);
}
private flattenUsersToGroups(levelGroups: GroupEntry[], allGroups: GroupEntry[]): Set<string> {
@@ -113,15 +139,15 @@ export class SyncService {
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);
@@ -138,35 +164,26 @@ export class SyncService {
}
}
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) {
const ig = new ImportDirectoryRequestGroup();
ig.name = g.name;
ig.externalId = g.externalId;
ig.users = Array.from(g.userMemberExternalIds);
model.groups.push(ig);
}
}
if (users != null) {
for (const u of users) {
const iu = new ImportDirectoryRequestUser();
iu.email = u.email;
if (iu.email != null) {
iu.email = iu.email.trim().toLowerCase();
}
iu.externalId = u.externalId;
iu.deleted = u.deleted || (removeDisabled && u.disabled);
model.users.push(iu);
}
}
return model;
private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean, overwriteExisting: boolean,
largeImport: boolean = false) {
return new OrganizationImportRequest({
groups: (groups ?? []).map(g => {
return {
name: g.name,
externalId: g.externalId,
memberExternalIds: Array.from(g.userMemberExternalIds),
};
}),
users: (users ?? []).map(u => {
return {
email: u.email,
externalId: u.externalId,
deleted: u.deleted || (removeDisabled && u.disabled),
};
}),
overwriteExisting: overwriteExisting,
largeImport: largeImport,
});
}
private async saveSyncTimes(syncConfig: SyncConfiguration, time: Date) {

View File

@@ -1,4 +1,4 @@
import { I18nService } from 'jslib/abstractions/i18n.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { SyncService } from './services/sync.service';

View File

@@ -8,58 +8,32 @@
"target": "ES2016",
"allowJs": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"types": [],
"baseUrl": ".",
"paths": {
"tldjs": [
"jslib/src/misc/tldjs.noop"
],
"jslib/*": [
"jslib/src/*"
"jslib-common/*": [
"jslib/common/src/*"
],
"@angular/*": [
"node_modules/@angular/*"
"jslib-angular/*": [
"jslib/angular/src/*"
],
"electron": [
"node_modules/electron"
"jslib-electron/*": [
"jslib/electron/src/*"
],
"node": [
"node_modules/@types/node"
],
"duo_web_sdk": [
"node_modules/duo_web_sdk"
"jslib-node/*": [
"jslib/node/src/*"
]
}
},
"angularCompilerOptions": {
"preserveWhitespaces": true
},
"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"
"include": [
"src",
"src-cli"
]
}

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"
}
]
}
}

View File

@@ -1,8 +1,9 @@
const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
if (process.env.NODE_ENV == null) {
process.env.NODE_ENV = 'development';
@@ -27,12 +28,12 @@ const moduleRules = [
];
const plugins = [
new CleanWebpackPlugin([
path.resolve(__dirname, 'build-cli/*'),
]),
new CopyWebpackPlugin([
{ from: './src/locales', to: 'locales' },
]),
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{ from: './src/locales', to: 'locales' },
],
}),
new webpack.DefinePlugin({
'process.env.BWCLI_ENV': JSON.stringify(ENV),
}),
@@ -59,12 +60,7 @@ const config = {
},
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',
},
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
symlinks: false,
modules: [path.resolve('node_modules')],
},

View File

@@ -1,8 +1,9 @@
const path = require('path');
const merge = require('webpack-merge');
const { merge } = require('webpack-merge');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const common = {
module: {
@@ -22,10 +23,7 @@ const common = {
plugins: [],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
jslib: path.join(__dirname, 'jslib/src'),
tldjs: path.join(__dirname, 'jslib/src/misc/tldjs.noop'),
},
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
},
output: {
filename: '[name].js',
@@ -55,14 +53,14 @@ const main = {
],
},
plugins: [
new CleanWebpackPlugin([
path.resolve(__dirname, 'build/*'),
]),
new CopyWebpackPlugin([
'./src/package.json',
{ from: './src/images', to: 'images' },
{ from: './src/locales', to: 'locales' },
]),
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
'./src/package.json',
{ from: './src/images', to: 'images' },
{ from: './src/locales', to: 'locales' },
],
}),
],
externals: [nodeExternals()],
};

View File

@@ -1,9 +1,10 @@
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const { merge } = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const common = {
module: {
@@ -33,9 +34,7 @@ const common = {
plugins: [],
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json'],
alias: {
jslib: path.join(__dirname, 'jslib/src'),
},
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
symlinks: false,
modules: [path.resolve('node_modules')],
},