1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-16 08:14:01 +00:00

Compare commits

..

4 Commits

Author SHA1 Message Date
Vince Grassia
7a35efc4dc Fix variable in versioninfo script (#156) 2021-09-22 14:20:30 -04:00
Joseph Flinn
653f2d4f0d CLI release job update (#155)
* updating the cli job in the release workflow to skip the building and use the latest rc build artifacts instead

* switching to downloading all of the artifacts

* renaming download step

* updating the artifact upload paths
2021-09-22 10:59:11 -07:00
Joseph Flinn
508cf1541b Version bump to 2.9.6 (#154) 2021-09-22 08:56:06 -07:00
Joseph Flinn
5c4294b66a Switching the AST install to a custom composite action (#153)
* Switching the AST install to a custom composite action

* fixing linter issues
2021-09-16 10:28:10 -07:00
106 changed files with 23318 additions and 16076 deletions

View File

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

View File

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

View File

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

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

View File

@@ -5,6 +5,8 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'l10n_master' - 'l10n_master'
workflow_dispatch:
inputs:
jobs: jobs:
@@ -13,13 +15,12 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up CLOC - name: Set up cloc
run: | run: |
sudo apt update sudo apt update
sudo apt -y install cloc sudo apt -y install cloc
- name: Print lines of code - name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
@@ -28,207 +29,30 @@ jobs:
name: Setup name: Setup
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
outputs: outputs:
package_version: ${{ steps.retrieve-version.outputs.package_version }} package_version: ${{ steps.get_version.outputs.package_version }}
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Get Package Version - name: Get Package Version
id: retrieve-version id: get_version
shell: pwsh
run: | run: |
PKG_VERSION=$(jq -r .version src/package.json) $env:pkgVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).version
echo "::set-output name=package_version::$PKG_VERSION" echo "::set-output name=package_version::$env:pkgVersion"
linux-cli: cli:
name: Build Linux CLI name: CLI
runs-on: ubuntu-20.04
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_PKG_FETCH_NODE_VERSION: 16.13.0
_PKG_FETCH_VERSION: 3.2
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '16'
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
- name: Get pkg-fetch
run: |
cd $HOME
fetchedUrl="https://github.com/vercel/pkg-fetch/releases/download/v$_PKG_FETCH_VERSION/node-v$_PKG_FETCH_NODE_VERSION-linux-x64"
mkdir -p .pkg-cache/v$_PKG_FETCH_VERSION
wget $fetchedUrl -O "./.pkg-cache/v$_PKG_FETCH_VERSION/fetched-v$_PKG_FETCH_NODE_VERSION-linux-x64"
- name: Keytar
run: |
keytarVersion=$(cat src/package.json | jq -r '.dependencies.keytar')
keytarTar="keytar-v$keytarVersion-napi-v3-linux-x64.tar"
keytarTarGz="$keytarTar.gz"
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
mkdir -p ./keytar/linux
wget $keytarUrl -O ./keytar/linux/$keytarTarGz
tar -xvf ./keytar/linux/$keytarTarGz -C ./keytar/linux
- name: Install
run: npm install
- name: Package CLI
run: npm run dist:cli:lin
- name: Zip
run: |
zip -j ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip ./dist-cli/linux/bwdc ./keytar/linux/build/Release/keytar.node
- name: Create checksums
run: |
sha256sum ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip | cut -d " " -f 1 > ./dist-cli/bwdc-linux-sha256-$_PACKAGE_VERSION.txt
- name: Version Test
run: |
sudo apt install libsecret-1-0 dbus-x11 gnome-keyring
eval $(dbus-launch --sh-syntax)
eval $(echo -n "" | /usr/bin/gnome-keyring-daemon --login)
eval $(/usr/bin/gnome-keyring-daemon --components=secrets --start)
mkdir -p test/linux
unzip ./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip -d ./test/linux
testVersion=$(./test/linux/bwdc -v)
echo "version: $_PACKAGE_VERSION"
echo "testVersion: $testVersion"
if [ "$testVersion" != "$_PACKAGE_VERSION" ]; then
echo "Version test failed."
exit 1
fi
- name: Upload Linux Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Linux checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
macos-cli:
name: Build Mac CLI
runs-on: macos-11
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_PKG_FETCH_NODE_VERSION: 16.13.0
_PKG_FETCH_VERSION: 3.2
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '16'
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
- name: Get pkg-fetch
run: |
cd $HOME
fetchedUrl="https://github.com/vercel/pkg-fetch/releases/download/v$_PKG_FETCH_VERSION/node-v$_PKG_FETCH_NODE_VERSION-macos-x64"
mkdir -p .pkg-cache/v$_PKG_FETCH_VERSION
wget $fetchedUrl -O "./.pkg-cache/v$_PKG_FETCH_VERSION/fetched-v$_PKG_FETCH_NODE_VERSION-macos-x64"
- name: Keytar
run: |
keytarVersion=$(cat src/package.json | jq -r '.dependencies.keytar')
keytarTar="keytar-v$keytarVersion-napi-v3-darwin-x64.tar"
keytarTarGz="$keytarTar.gz"
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
mkdir -p ./keytar/macos
wget $keytarUrl -O ./keytar/macos/$keytarTarGz
tar -xvf ./keytar/macos/$keytarTarGz -C ./keytar/macos
- name: Install
run: npm install
- name: Package CLI
run: npm run dist:cli:mac
- name: Zip
run: |
zip -j ./dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip ./dist-cli/macos/bwdc ./keytar/macos/build/Release/keytar.node
- name: Create checksums
run: |
sha256sum ./dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip | cut -d " " -f 1 > ./dist-cli/bwdc-macos-sha256-$_PACKAGE_VERSION.txt
- name: Version Test
run: |
mkdir -p test/macos
unzip ./dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip -d ./test/macos
testVersion=$(./test/macos/bwdc -v)
echo "version: $_PACKAGE_VERSION"
echo "testVersion: $testVersion"
if [ "$testVersion" != "$_PACKAGE_VERSION" ]; then
echo "Version test failed."
exit 1
fi
- name: Upload Mac Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Mac checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
windows-cli:
name: Build Windows CLI
runs-on: windows-2019 runs-on: windows-2019
needs: setup needs: setup
env: env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_WIN_PKG_FETCH_VERSION: 16.13.0 _WIN_PKG_FETCH_VERSION: 14.17.0
_WIN_PKG_VERSION: 3.2 _WIN_PKG_VERSION: 3.1
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Setup Windows builder - name: Setup Windows builder
run: | run: |
@@ -238,10 +62,11 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: '16' node-version: '14.x'
- name: Update NPM - name: Update NPM
run: | run: |
npm install -g npm@7
npm install -g node-gyp npm install -g node-gyp
node-gyp install $(node -v) node-gyp install $(node -v)
@@ -253,61 +78,36 @@ jobs:
New-Item -ItemType directory -Path ./.pkg-cache New-Item -ItemType directory -Path ./.pkg-cache
New-Item -ItemType directory -Path ./.pkg-cache/v$env:_WIN_PKG_VERSION New-Item -ItemType directory -Path ./.pkg-cache/v$env:_WIN_PKG_VERSION
Invoke-RestMethod -Uri $fetchedUrl ` Invoke-RestMethod -Uri $fetchedUrl -OutFile "./.pkg-cache/v$env:_WIN_PKG_VERSION/fetched-v$env:_WIN_PKG_FETCH_VERSION-win-x64"
-OutFile "./.pkg-cache/v$env:_WIN_PKG_VERSION/fetched-v$env:_WIN_PKG_FETCH_VERSION-win-x64"
- name: Keytar - name: Keytar
shell: pwsh shell: pwsh
run: | run: |
$keytarVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).dependencies.keytar $keytarVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).dependencies.keytar
$keytarTar = "keytar-v${keytarVersion}-napi-v3-{0}-x64.tar" $nodeModVersion = node -e "console.log(process.config.variables.node_module_version)"
$keytarTar = "keytar-v${keytarVersion}-node-v${nodeModVersion}-{0}-x64.tar"
$keytarTarGz = "${keytarTar}.gz" $keytarTarGz = "${keytarTar}.gz"
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}" $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 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")" 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/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" 7z e "./keytar/windows/$($keytarTar -f "win32")" -o"./keytar/windows"
- name: Setup Version Info - name: Setup Version Info
shell: pwsh shell: pwsh
run: | run: ./scripts/make-versioninfo.ps1
$major, $minor, $patch = $env:_PACKAGE_VERSION.split('.')
$versionInfo = @"
1 VERSIONINFO
FILEVERSION $major,$minor,$patch,0
PRODUCTVERSION $major,$minor,$patch,0
FILEOS 0x40004
FILETYPE 0x1
{
BLOCK "StringFileInfo"
{
BLOCK "040904b0"
{
VALUE "CompanyName", "Bitwarden Inc."
VALUE "ProductName", "Bitwarden"
VALUE "FileDescription", "Bitwarden Directory Connector CLI"
VALUE "FileVersion", "$env:_PACKAGE_VERSION"
VALUE "ProductVersion", "$env:_PACKAGE_VERSION"
VALUE "OriginalFilename", "bwdc.exe"
VALUE "InternalName", "bwdc"
VALUE "LegalCopyright", "Copyright Bitwarden Inc."
}
}
BLOCK "VarFileInfo"
{
VALUE "Translation", 0x0409 0x04B0
}
}
"@
$versionInfo | Out-File ./version-info.rc
- name: Resource Hacker - name: Resource Hacker
shell: cmd shell: cmd
@@ -324,12 +124,14 @@ jobs:
run: npm install run: npm install
- name: Package CLI - name: Package CLI
run: npm run dist:cli:win run: npm run dist:cli
- name: Zip - name: Zip
shell: cmd shell: cmd
run: | run: |
7z a ./dist-cli/bwdc-windows-%_PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node 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 - name: Version Test
run: | run: |
@@ -345,41 +147,63 @@ jobs:
run: | run: |
checksum -f="./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" ` checksum -f="./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:_PACKAGE_VERSION}.txt -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 - name: Upload windows zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3 uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
with: with:
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Windows checksum to GitHub - name: Upload mac zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3 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
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
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
with: with:
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
- name: Upload mac checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
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
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: windows_gui:
name: Build Windows GUI name: Windows GUI
runs-on: windows-2019 runs-on: windows-2019
needs: setup needs: setup
env: env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps: steps:
- name: Set up .NET
uses: actions/setup-dotnet@a71d1eb2c86af85faa8c772c03fb365e377e45ea
with:
dotnet-version: "3.1.x"
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: '16' node-version: '14.x'
- name: Update NPM - name: Update NPM
run: | run: |
npm install -g npm@7
npm install -g node-gyp npm install -g node-gyp
node-gyp install $(node -v) node-gyp install $(node -v)
@@ -397,13 +221,13 @@ jobs:
uses: bitwarden/gh-actions/install-ast@f135c42c8596cb535c5bcb7523c0b2eef89709ac uses: bitwarden/gh-actions/install-ast@f135c42c8596cb535c5bcb7523c0b2eef89709ac
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Install Node dependencies - name: Install Node dependencies
run: npm install run: npm install
# - name: Run linter - name: Run linter
# run: npm run lint run: npm run lint
- name: Build & Sign - name: Build & Sign
run: npm run dist:win run: npm run dist:win
@@ -415,37 +239,24 @@ jobs:
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }} SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }} SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
- name: Upload Portable Executable to GitHub - name: List Dist
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3 run: dir ./dist
- name: Publish Portable Exe to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
with: with:
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable to GitHub - name: Publish Installer Exe to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3 uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
with: with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable Blockmap to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: latest.yml
path: ./dist/latest.yml
if-no-files-found: error
linux-gui: linux:
name: Build Linux GUI name: Linux
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: setup needs: setup
env: env:
@@ -454,10 +265,11 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: '16' node-version: '14.x'
- name: Update NPM - name: Update NPM
run: | run: |
npm install -g npm@7
npm install -g node-gyp npm install -g node-gyp
node-gyp install $(node -v) node-gyp install $(node -v)
@@ -471,34 +283,26 @@ jobs:
sudo apt-get -y install rpm sudo apt-get -y install rpm
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: NPM Install - name: NPM install
run: npm install run: npm install
- name: NPM Rebuild - name: NPM rebuild
run: npm run rebuild run: npm run rebuild
- name: NPM Package - name: NPM package
run: npm run dist:lin run: npm run dist:lin
- name: Upload AppImage - name: Publish AppImage
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3 uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
with: with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
if-no-files-found: error
macos-gui: macos:
name: Build MacOS GUI name: MacOS
runs-on: macos-11 runs-on: macos-11
needs: setup needs: setup
env: env:
@@ -507,10 +311,11 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: '16' node-version: '14.x'
- name: Update NPM - name: Update NPM
run: | run: |
npm install -g npm@7
npm install -g node-gyp npm install -g node-gyp
node-gyp install $(node -v) node-gyp install $(node -v)
@@ -521,168 +326,61 @@ jobs:
run: | run: |
node --version node --version
npm --version npm --version
echo "GitHub ref: $GITHUB_REF" Write-Output "GitHub ref: $env:GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT" Write-Output "GitHub event: $env:GITHUB_EVENT"
shell: bash shell: pwsh
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Decrypt secrets - name: Decrypt secrets
run: ./.github/scripts/macos/decrypt-secrets.ps1
shell: pwsh
env: env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }} DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
shell: bash
run: |
mkdir -p $HOME/secrets
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output "$HOME/secrets/devid-app-cert.p12" \
"$GITHUB_WORKSPACE/.github/secrets/devid-app-cert.p12.gpg"
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output "$HOME/secrets/devid-installer-cert.p12" \
"$GITHUB_WORKSPACE/.github/secrets/devid-installer-cert.p12.gpg"
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output "$HOME/secrets/macdev-cert.p12" \
"$GITHUB_WORKSPACE/.github/secrets/macdev-cert.p12.gpg"
- name: Set up keychain - name: Set up keychain
run: ./.github/scripts/macos/setup-keychain.ps1
shell: pwsh
env: env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
DEVID_CERT_PASSWORD: ${{ secrets.DEVID_CERT_PASSWORD }} DEVID_CERT_PASSWORD: ${{ secrets.DEVID_CERT_PASSWORD }}
MACDEV_CERT_PASSWORD: ${{ secrets.MACDEV_CERT_PASSWORD }} MACDEV_CERT_PASSWORD: ${{ secrets.MACDEV_CERT_PASSWORD }}
shell: bash
run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
security set-keychain-settings -lut 1200 build.keychain
security import "$HOME/secrets/devid-app-cert.p12" -k build.keychain -P $DEVID_CERT_PASSWORD \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
security import "$HOME/secrets/devid-installer-cert.p12" -k build.keychain -P $DEVID_CERT_PASSWORD \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
security import "$HOME/secrets/macdev-cert.p12" -k build.keychain -P $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 $KEYCHAIN_PASSWORD build.keychain
- name: Load package version - name: Load package version
run: | run: ./.github/scripts/load-version.ps1
$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;
shell: pwsh shell: pwsh
- name: Install Node dependencies - name: Install Node dependencies
run: npm install run: npm install
# - name: Run linter - name: Run linter
# run: npm run lint run: npm run lint
- name: Build application - 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 run: npm run dist:mac
env: env:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
- name: Rename Zip Artifact
run: |
cd dist
mv "Bitwarden Directory Connector-${{ env._PACKAGE_VERSION }}-mac.zip" \
"Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip"
- name: Upload .zip artifact - name: Upload .zip artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3 if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
with: with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact - name: Upload .dmg artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3 if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
with: with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
if-no-files-found: error
- name: Upload .dmg Blockmap artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: latest-mac.yml
path: ./dist/latest-mac.yml
if-no-files-found: error
check-failures:
name: Check for failures
runs-on: ubuntu-20.04
needs:
- cloc
- setup
- linux-cli
- macos-cli
- windows-cli
- windows-gui
- linux-gui
- macos-gui
steps:
- name: Check if any job failed
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
env:
CLOC_STATUS: ${{ needs.cloc.result }}
SETUP_STATUS: ${{ needs.setup.result }}
LINUX_CLI_STATUS: ${{ needs.linux-cli.result }}
MACOS_CLI_STATUS: ${{ needs.macos-cli.result }}
WINDOWS_CLI_STATUS: ${{ needs.windows-cli.result }}
WINDOWS_GUI_STATUS: ${{ needs.windows-gui.result }}
LINUX_GUI_STATUS: ${{ needs.linux-gui.result }}
MACOS_GUI_STATUS: ${{ needs.macos-gui.result }}
run: |
if [ "$CLOC_STATUS" = "failure" ]; then
exit 1
elif [ "$SETUP_STATUS" = "failure" ]; then
exit 1
elif [ "$LINUX_CLI_STATUS" = "failure" ]; then
exit 1
elif [ "$MACOS_CLI_STATUS" = "failure" ]; then
exit 1
elif [ "$WINDOWS_CLI_STATUS" = "failure" ]; then
exit 1
elif [ "$WINDOWS_GUI_STATUS" = "failure" ]; then
exit 1
elif [ "$LINUX_GUI_STATUS" = "failure" ]; then
exit 1
elif [ "$MACOS_GUI_STATUS" = "failure" ]; then
exit 1
fi
- name: Login to Azure - Prod Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
if: failure()
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
if: failure()
with:
keyvault: "bitwarden-prod-kv"
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
with:
status: ${{ job.status }}

View File

@@ -4,25 +4,25 @@ name: Release
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
release_type: release_tag_name_input:
description: 'Release Options' description: "Release Tag Name <X.X.X>"
required: true required: true
default: 'Initial Release'
type: choice
options:
- Initial Release
- Redeploy
jobs: jobs:
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
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: steps:
- name: Branch check - name: Branch check
run: | run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then if [[ "$GITHUB_REF" != "refs/heads/rc" ]]; then
echo "===================================" echo "==================================="
echo "[!] Can only release from the 'rc' or 'hotfix' branches" echo "[!] Can only release from rc branch"
echo "===================================" echo "==================================="
exit 1 exit 1
fi fi
@@ -30,65 +30,286 @@ jobs:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Retrieve Directory Connector release version - name: Create Release Vars
id: retrieve-version id: create_tags
run: | run: |
PKG_VERSION=$(jq -r .version src/package.json) case "${RELEASE_TAG_NAME_INPUT:0:1}" in
echo "::set-output name=package_version::$PKG_VERSION" 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: Check to make sure Mobile release version has been bumped - name: Create Draft Release
if: ${{ github.event.inputs.release_type == 'Initial Release' }} id: create_release
uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | with:
latest_ver=$(hub release -L 1 -f '%T') tag_name: ${{ env.RELEASE_TAG_NAME }}
latest_ver=${latest_ver:1} release_name: ${{ env.RELEASE_NAME }}
echo "Latest version: $latest_ver" draft: true
ver=${{ steps.retrieve-version.outputs.package_version }} prerelease: false
echo "Version: $ver"
if [ "$latest_ver" = "$ver" ]; then
echo "Version has not been bumped!"
exit 1
fi
shell: bash
- name: Get branch name cli:
id: branch name: CLI
run: | runs-on: windows-2019
BRANCH_NAME=$(basename ${{ github.ref }}) needs: setup
echo "::set-output name=branch-name::$BRANCH_NAME" env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Download all artifacts - name: Download latest RC build artifacts
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783 uses: dawidd6/action-download-artifact@b9571484721e8187f1fd08147b497129f8972c74 # v2.14.0
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
branch: ${{ steps.branch.outputs.branch-name }} branch: rc
path: ./dist-cli
- name: Create release - name: Upload Windows zip release asset
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09 # v2.8.5 uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5
env: env:
PKG_VERSION: ${{ steps.retrieve-version.outputs.package_version }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
artifacts: "./bwdc-windows-${{ env.PKG_VERSION }}.zip, upload_url: ${{ needs.setup.outputs.release_upload_url }}
./bwdc-macos-${{ env.PKG_VERSION }}.zip, asset_path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
./bwdc-linux-${{ env.PKG_VERSION }}.zip, asset_name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
./bwdc-windows-sha256-${{ env.PKG_VERSION }}.txt, asset_content_type: application/zip
./bwdc-macos-sha256-${{ env.PKG_VERSION }}.txt,
./bwdc-linux-sha256-${{ env.PKG_VERSION }}.txt, - name: Upload MacOS zip release asset
./Bitwarden-Connector-Portable-${{ env.PKG_VERSION }}.exe, uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe, env:
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe.blockmap, GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
./Bitwarden-Connector-${{ env.PKG_VERSION }}-x86_64.AppImage, with:
./Bitwarden-Connector-${{ env.PKG_VERSION }}-mac.zip, upload_url: ${{ needs.setup.outputs.release_upload_url }}
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg, asset_path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg.blockmap, asset_name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
./latest-linux.yml, asset_content_type: application/zip
./latest-mac.yml,
./latest.yml" - name: Upload Linux zip release asset
commit: ${{ github.sha }} uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5
tag: v${{ env.PKG_VERSION }} env:
name: Version ${{ env.PKG_VERSION }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
body: "<insert release notes here>" with:
token: ${{ secrets.GITHUB_TOKEN }} upload_url: ${{ needs.setup.outputs.release_upload_url }}
draft: true asset_path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip/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/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/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/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
asset_name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
asset_content_type: text/plain
windows-gui:
name: Windows GUI
runs-on: windows-2019
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" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
shell: pwsh
- name: Print environment
run: |
node --version
npm --version
dotnet --version
- name: Install AST
uses: bitwarden/gh-actions/install-ast@f135c42c8596cb535c5bcb7523c0b2eef89709ac
- 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:
name: Linux
runs-on: ubuntu-20.04
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:
name: MacOS
runs-on: macos-11
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 }}

View File

@@ -1,65 +0,0 @@
---
name: Version Bump
on:
workflow_dispatch:
inputs:
version_number:
description: "New Version"
required: true
jobs:
bump_version:
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
runs-on: ubuntu-20.04
steps:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Create Version Branch
run: |
git switch -c version_bump_${{ github.event.inputs.version_number }}
git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Checkout Version Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
with:
ref: version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - Package
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/package.json"
- name: Commit files
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
- name: Push changes
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Create Version PR
env:
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
BASE_BRANCH: master
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
run: |
gh pr create --title "$TITLE" \
--base "$BASE" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
--body "
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
Automated version bump to ${{ github.event.inputs.version_number }}"

1
.husky/.gitignore vendored
View File

@@ -1 +0,0 @@
_

View File

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

View File

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

View File

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

14
.vscode/launch.json vendored
View File

@@ -7,7 +7,10 @@
"name": "Electron: Main", "name": "Electron: Main",
"protocol": "inspector", "protocol": "inspector",
"cwd": "${workspaceRoot}/build", "cwd": "${workspaceRoot}/build",
"runtimeArgs": ["--remote-debugging-port=9223", "."], "runtimeArgs": [
"--remote-debugging-port=9223",
"."
],
"windows": { "windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
}, },
@@ -28,13 +31,18 @@
"protocol": "inspector", "protocol": "inspector",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/build-cli/bwdc.js", "program": "${workspaceFolder}/build-cli/bwdc.js",
"args": ["sync"] "args": [
"sync"
]
} }
], ],
"compounds": [ "compounds": [
{ {
"name": "Electron: All", "name": "Electron: All",
"configurations": ["Electron: Main", "Electron: Renderer"] "configurations": [
"Electron: Main",
"Electron: Renderer"
]
} }
] ]
} }

View File

@@ -6,7 +6,6 @@
The Bitwarden Directory Connector is a a desktop application used to sync your Bitwarden enterprise organization to an existing directory of users and groups. The Bitwarden Directory Connector is a a desktop application used to sync your Bitwarden enterprise organization to an existing directory of users and groups.
Supported directories: Supported directories:
- Active Directory - Active Directory
- Any other LDAP-based directory - Any other LDAP-based directory
- Azure Active Directory - Azure Active Directory
@@ -48,7 +47,7 @@ We provide detailed documentation and examples for using the Directory Connector
**Requirements** **Requirements**
- [Node.js](https://nodejs.org) v16.13.1 (LTS) - [Node.js](https://nodejs.org) v14
- Windows users: To compile the native node modules used in the app you will need the Visual C++ toolset, available through the standard Visual Studio installer (recommended) or by installing [`windows-build-tools`](https://github.com/felixrieseberg/windows-build-tools) through `npm`. See more at [Compiling native Addon modules](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules). - Windows users: To compile the native node modules used in the app you will need the Visual C++ toolset, available through the standard Visual Studio installer (recommended) or by installing [`windows-build-tools`](https://github.com/felixrieseberg/windows-build-tools) through `npm`. See more at [Compiling native Addon modules](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules).
**Run the app** **Run the app**
@@ -79,23 +78,3 @@ node ./build-cli/bwdc.js --help
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file. Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file. Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
### Prettier
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
1. Check out your local Branch
2. Run `git merge 225073aa335d33ad905877b68336a9288e89ea10`
3. Resolve any merge conflicts, commit.
4. Run `npm run prettier`
5. Commit
6. Run `git merge -Xours 096196fcd512944d1c3d9c007647a1319b032639`
7. Push
#### Git blame
We also recommend that you configure git to ignore the prettier revision using:
```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

25
gulpfile.js Normal file
View File

@@ -0,0 +1,25 @@
const gulp = require('gulp');
const googleWebFonts = require('gulp-google-webfonts');
const del = require('del');
const paths = {
cssDir: './src/css/',
};
function clean() {
return del([paths.cssDir]);
}
function webfonts() {
return gulp.src('./webfonts.list')
.pipe(googleWebFonts({
fontsDir: 'webfonts',
cssFilename: 'webfonts.css',
format: 'woff',
}))
.pipe(gulp.dest(paths.cssDir));
}
exports.clean = clean;
exports.webfonts = gulp.series(clean, webfonts);
exports['prebuild:renderer'] = webfonts;;

2
jslib

Submodule jslib updated: e0cc754d6f...c70c8ecc24

23452
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
exports.default = async function (configuration) { exports.default = async function(configuration) {
if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") { if (
console.log(`[*] Signing file: ${configuration.path}`); parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 &&
configuration.path.slice(-4) == ".exe"
) {
console.log(`[*] Signing file: ${configuration.path}`)
require("child_process").execSync( require("child_process").execSync(
`azuresigntool sign ` + `azuresigntool sign ` +
`-kvu ${process.env.SIGNING_VAULT_URL} ` + `-kvu ${process.env.SIGNING_VAULT_URL} ` +
@@ -13,7 +16,7 @@ exports.default = async function (configuration) {
`-tr http://timestamp.digicert.com ` + `-tr http://timestamp.digicert.com ` +
`"${configuration.path}"`, `"${configuration.path}"`,
{ {
stdio: "inherit", stdio: "inherit"
} }
); );
} }

1006
src-cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,33 @@
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
} from '@angular/core';
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; import { ToasterService } from 'angular2-toaster';
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 { SyncService } from "../../services/sync.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 { GroupEntry } from "../../models/groupEntry"; import { SyncService } from '../../services/sync.service';
import { SimResult } from "../../models/simResult";
import { UserEntry } from "../../models/userEntry";
import { ConnectorUtils } from "../../utils"; import { GroupEntry } from '../../models/groupEntry';
import { SimResult } from '../../models/simResult';
import { UserEntry } from '../../models/userEntry';
import { ConfigurationService } from '../../services/configuration.service';
import { StateService } from "../../abstractions/state.service"; import { BroadcasterService } from 'jslib-angular/services/broadcaster.service';
const BroadcasterSubscriptionId = "DashboardComponent"; import { ConnectorUtils } from '../../utils';
const BroadcasterSubscriptionId = 'DashboardComponent';
@Component({ @Component({
selector: "app-dashboard", selector: 'app-dashboard',
templateUrl: "dashboard.component.html", templateUrl: 'dashboard.component.html',
}) })
export class DashboardComponent implements OnInit, OnDestroy { export class DashboardComponent implements OnInit, OnDestroy {
simGroups: GroupEntry[]; simGroups: GroupEntry[];
@@ -35,22 +43,17 @@ export class DashboardComponent implements OnInit, OnDestroy {
lastUserSync: Date; lastUserSync: Date;
syncRunning: boolean; syncRunning: boolean;
constructor( constructor(private i18nService: I18nService, private syncService: SyncService,
private i18nService: I18nService, private configurationService: ConfigurationService, private broadcasterService: BroadcasterService,
private syncService: SyncService, private ngZone: NgZone, private messagingService: MessagingService,
private broadcasterService: BroadcasterService, private toasterService: ToasterService, private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone, private stateService: StateService) { }
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
private changeDetectorRef: ChangeDetectorRef,
private stateService: StateService
) {}
async ngOnInit() { async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => { this.ngZone.run(async () => {
switch (message.command) { switch (message.command) {
case "dirSyncCompleted": case 'dirSyncCompleted':
this.updateLastSync(); this.updateLastSync();
break; break;
default: default:
@@ -61,7 +64,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
}); });
}); });
this.syncRunning = !!(await this.stateService.getSyncingDir()); this.syncRunning = !!(await this.stateService.get('syncingDir'));
this.updateLastSync(); this.updateLastSync();
} }
@@ -72,15 +75,15 @@ export class DashboardComponent implements OnInit, OnDestroy {
async start() { async start() {
this.startPromise = this.syncService.sync(false, false); this.startPromise = this.syncService.sync(false, false);
await this.startPromise; await this.startPromise;
this.messagingService.send("scheduleNextDirSync"); this.messagingService.send('scheduleNextDirSync');
this.syncRunning = true; this.syncRunning = true;
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingStarted")); this.toasterService.popAsync('success', null, this.i18nService.t('syncingStarted'));
} }
async stop() { async stop() {
this.messagingService.send("cancelDirSync"); this.messagingService.send('cancelDirSync');
this.syncRunning = false; this.syncRunning = false;
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingStopped")); this.toasterService.popAsync('success', null, this.i18nService.t('syncingStopped'));
} }
async sync() { async sync() {
@@ -88,11 +91,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
const result = await this.syncPromise; const result = await this.syncPromise;
const groupCount = result[0] != null ? result[0].length : 0; const groupCount = result[0] != null ? result[0].length : 0;
const userCount = result[1] != null ? result[1].length : 0; const userCount = result[1] != null ? result[1].length : 0;
this.platformUtilsService.showToast( this.toasterService.popAsync('success', null,
"success", this.i18nService.t('syncCounts', groupCount.toString(), userCount.toString()));
null,
this.i18nService.t("syncCounts", groupCount.toString(), userCount.toString())
);
} }
async simulate() { async simulate() {
@@ -103,11 +103,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.simDeletedUsers = []; this.simDeletedUsers = [];
try { try {
this.simPromise = ConnectorUtils.simulate( this.simPromise = ConnectorUtils.simulate(this.syncService, this.i18nService, this.simSinceLast);
this.syncService,
this.i18nService,
this.simSinceLast
);
const result = await this.simPromise; const result = await this.simPromise;
this.simGroups = result.groups; this.simGroups = result.groups;
this.simUsers = result.users; this.simUsers = result.users;
@@ -121,7 +117,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
} }
private async updateLastSync() { private async updateLastSync() {
this.lastGroupSync = await this.stateService.getLastGroupSync(); this.lastGroupSync = await this.configurationService.getLastGroupSyncDate();
this.lastUserSync = await this.stateService.getLastUserSync(); this.lastUserSync = await this.configurationService.getLastUserSyncDate();
} }
} }

View File

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

View File

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

View File

@@ -1,245 +1,132 @@
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
<div class="card mb-3"> <div class="card mb-3">
<h3 class="card-header">{{ "directory" | i18n }}</h3> <h3 class="card-header">{{'directory' | i18n}}</h3>
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="directory">{{ "type" | i18n }}</label> <label for="directory">{{'type' | i18n}}</label>
<select class="form-control" id="directory" name="Directory" [(ngModel)]="directory"> <select class="form-control" id="directory" name="Directory" [(ngModel)]="directory">
<option *ngFor="let o of directoryOptions" [ngValue]="o.value"> <option *ngFor="let o of directoryOptions" [ngValue]="o.value">{{o.name}}</option>
{{ o.name }}
</option>
</select> </select>
</div> </div>
<div [hidden]="directory != directoryType.Ldap"> <div [hidden]="directory != directoryType.Ldap">
<div class="form-group"> <div class="form-group">
<label for="hostname">{{ "serverHostname" | i18n }}</label> <label for="hostname">{{'serverHostname' | i18n}}</label>
<input <input type="text" class="form-control" id="hostname" name="Hostname"
type="text" [(ngModel)]="ldap.hostname">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} ad.company.com</small>
id="hostname"
name="Hostname"
[(ngModel)]="ldap.hostname"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} ad.company.com</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="port">{{ "port" | i18n }}</label> <label for="port">{{'port' | i18n}}</label>
<input type="text" class="form-control" id="port" name="Port" [(ngModel)]="ldap.port" /> <input type="text" class="form-control" id="port" name="Port" [(ngModel)]="ldap.port">
<small class="text-muted form-text">{{ "ex" | i18n }} 389</small> <small class="text-muted form-text">{{'ex' | i18n}} 389</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="rootPath">{{ "rootPath" | i18n }}</label> <label for="rootPath">{{'rootPath' | i18n}}</label>
<input <input type="text" class="form-control" id="rootPath" name="RootPath"
type="text" [(ngModel)]="ldap.rootPath">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} dc=company,dc=com</small>
id="rootPath"
name="RootPath"
[(ngModel)]="ldap.rootPath"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} dc=company,dc=com</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="ad" [(ngModel)]="ldap.ad" name="AD">
class="form-check-input" <label class="form-check-label" for="ad">{{'ldapAd' | i18n}}</label>
type="checkbox"
id="ad"
[(ngModel)]="ldap.ad"
name="AD"
/>
<label class="form-check-label" for="ad">{{ "ldapAd" | i18n }}</label>
</div> </div>
</div> </div>
<div class="form-group" *ngIf="!ldap.ad"> <div class="form-group" *ngIf="!ldap.ad">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="pagedSearch"
class="form-check-input" [(ngModel)]="ldap.pagedSearch" name="PagedSearch">
type="checkbox" <label class="form-check-label" for="pagedSearch">{{'ldapPagedResults' | i18n}}</label>
id="pagedSearch"
[(ngModel)]="ldap.pagedSearch"
name="PagedSearch"
/>
<label class="form-check-label" for="pagedSearch">{{
"ldapPagedResults" | i18n
}}</label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="ldapEncrypted" [(ngModel)]="ldap.ssl"
class="form-check-input" name="Encrypted">
type="checkbox" <label class="form-check-label" for="ldapEncrypted">{{'ldapEncrypted' | i18n}}</label>
id="ldapEncrypted"
[(ngModel)]="ldap.ssl"
name="Encrypted"
/>
<label class="form-check-label" for="ldapEncrypted">{{
"ldapEncrypted" | i18n
}}</label>
</div> </div>
</div> </div>
<div class="ml-4" *ngIf="ldap.ssl"> <div class="ml-4" *ngIf="ldap.ssl">
<div class="form-group"> <div class="form-group">
<div class="form-radio"> <div class="form-radio">
<input <input class="form-radio-input" type="radio" [value]="false" id="ssl"
class="form-radio-input" [(ngModel)]="ldap.startTls" name="SSL">
type="radio" <label class="form-radio-label" for="ssl">{{'ldapSsl' | i18n}}</label>
[value]="false"
id="ssl"
[(ngModel)]="ldap.startTls"
name="SSL"
/>
<label class="form-radio-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
</div> </div>
<div class="form-radio"> <div class="form-radio">
<input <input class="form-radio-input" type="radio" [value]="true" id="startTls"
class="form-radio-input" [(ngModel)]="ldap.startTls" name="StartTLS">
type="radio" <label class="form-radio-label" for="startTls">{{'ldapTls' | i18n}}</label>
[value]="true"
id="startTls"
[(ngModel)]="ldap.startTls"
name="StartTLS"
/>
<label class="form-radio-label" for="startTls">{{ "ldapTls" | i18n }}</label>
</div> </div>
</div> </div>
<div class="ml-4" *ngIf="ldap.startTls"> <div class="ml-4" *ngIf="ldap.startTls">
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p> <p>{{'ldapTlsUntrustedDesc' | i18n}}</p>
<div class="form-group"> <div class="form-group">
<label for="tlsCaPath">{{ "ldapTlsCa" | i18n }}</label> <label for="tlsCaPath">{{'ldapTlsCa' | i18n}}</label>
<input <input type="file" class="form-control-file mb-2" id="tlsCaPath_file"
type="file" (change)="setSslPath('tlsCaPath')">
class="form-control-file mb-2" <input type="text" class="form-control" id="tlsCaPath" name="TLSCaPath"
id="tlsCaPath_file" [(ngModel)]="ldap.tlsCaPath">
(change)="setSslPath('tlsCaPath')"
/>
<input
type="text"
class="form-control"
id="tlsCaPath"
name="TLSCaPath"
[(ngModel)]="ldap.tlsCaPath"
/>
</div> </div>
</div> </div>
<div class="ml-4" *ngIf="!ldap.startTls"> <div class="ml-4" *ngIf="!ldap.startTls">
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p> <p>{{'ldapSslUntrustedDesc' | i18n}}</p>
<div class="form-group"> <div class="form-group">
<label for="sslCertPath">{{ "ldapSslCert" | i18n }}</label> <label for="sslCertPath">{{'ldapSslCert' | i18n}}</label>
<input <input type="file" class="form-control-file mb-2" id="sslCertPath_file"
type="file" (change)="setSslPath('sslCertPath')">
class="form-control-file mb-2" <input type="text" class="form-control" id="sslCertPath" name="SSLCertPath"
id="sslCertPath_file" [(ngModel)]="ldap.sslCertPath">
(change)="setSslPath('sslCertPath')"
/>
<input
type="text"
class="form-control"
id="sslCertPath"
name="SSLCertPath"
[(ngModel)]="ldap.sslCertPath"
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="sslKeyPath">{{ "ldapSslKey" | i18n }}</label> <label for="sslKeyPath">{{'ldapSslKey' | i18n}}</label>
<input <input type="file" class="form-control-file mb-2" id="sslKeyPath_file"
type="file" (change)="setSslPath('sslKeyPath')">
class="form-control-file mb-2" <input type="text" class="form-control" id="sslKeyPath" name="SSLKeyPath"
id="sslKeyPath_file" [(ngModel)]="ldap.sslKeyPath">
(change)="setSslPath('sslKeyPath')"
/>
<input
type="text"
class="form-control"
id="sslKeyPath"
name="SSLKeyPath"
[(ngModel)]="ldap.sslKeyPath"
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="sslCaPath">{{ "ldapSslCa" | i18n }}</label> <label for="sslCaPath">{{'ldapSslCa' | i18n}}</label>
<input <input type="file" class="form-control-file mb-2" id="sslCaPath_file"
type="file" (change)="setSslPath('sslCaPath')">
class="form-control-file mb-2" <input type="text" class="form-control" id="sslCaPath" name="SSLCaPath"
id="sslCaPath_file" [(ngModel)]="ldap.sslCaPath">
(change)="setSslPath('sslCaPath')"
/>
<input
type="text"
class="form-control"
id="sslCaPath"
name="SSLCaPath"
[(ngModel)]="ldap.sslCaPath"
/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="certDoNotVerify"
class="form-check-input" [(ngModel)]="ldap.sslAllowUnauthorized" name="CertDoNoVerify">
type="checkbox" <label class="form-check-label" for="certDoNotVerify">{{'ldapCertDoNotVerify' |
id="certDoNotVerify" i18n}}</label>
[(ngModel)]="ldap.sslAllowUnauthorized"
name="CertDoNoVerify"
/>
<label class="form-check-label" for="certDoNotVerify">{{
"ldapCertDoNotVerify" | i18n
}}</label>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group" [hidden]="true"> <div class="form-group" [hidden]="true">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="currentUser"
class="form-check-input" [(ngModel)]="ldap.currentUser" name="CurrentUser">
type="checkbox" <label class="form-check-label" for="currentUser">{{'currentUser' | i18n}}</label>
id="currentUser"
[(ngModel)]="ldap.currentUser"
name="CurrentUser"
/>
<label class="form-check-label" for="currentUser">{{ "currentUser" | i18n }}</label>
</div> </div>
</div> </div>
<div [hidden]="ldap.currentUser"> <div [hidden]="ldap.currentUser">
<div class="form-group"> <div class="form-group">
<label for="username">{{ "username" | i18n }}</label> <label for="username">{{'username' | i18n}}</label>
<input <input type="text" class="form-control" id="username" name="Username"
type="text" [(ngModel)]="ldap.username">
class="form-control" <small class="text-muted form-text" *ngIf="ldap.ad">{{'ex' | i18n}} company\admin</small>
id="username" <small class="text-muted form-text" *ngIf="!ldap.ad">{{'ex' | i18n}}
name="Username" cn=admin,dc=company,dc=com</small>
[(ngModel)]="ldap.username"
/>
<small class="text-muted form-text" *ngIf="ldap.ad"
>{{ "ex" | i18n }} company\admin</small
>
<small class="text-muted form-text" *ngIf="!ldap.ad"
>{{ "ex" | i18n }} cn=admin,dc=company,dc=com</small
>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">{{ "password" | i18n }}</label> <label for="password">{{'password' | i18n}}</label>
<div class="input-group"> <div class="input-group">
<input <input type="{{showLdapPassword ? 'text' : 'password'}}" class="form-control" id="password" name="Password"
type="{{ showLdapPassword ? 'text' : 'password' }}" [(ngModel)]="ldap.password">
class="form-control"
id="password"
name="Password"
[(ngModel)]="ldap.password"
/>
<div class="input-group-append"> <div class="input-group-append">
<button <button type="button" class="btn btn-outline-secondary" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleLdapPassword()">
type="button" <i class="fa fa-lg" aria-hidden="true"[ngClass]="showLdapPassword ? 'fa-eye-slash' : 'fa-eye'"></i>
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleLdapPassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showLdapPassword ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button> </button>
</div> </div>
</div> </div>
@@ -248,60 +135,23 @@
</div> </div>
<div [hidden]="directory != directoryType.AzureActiveDirectory"> <div [hidden]="directory != directoryType.AzureActiveDirectory">
<div class="form-group"> <div class="form-group">
<label for="identityAuthority">{{ "identityAuthority" | i18n }}</label> <label for="tenant">{{'tenant' | i18n}}</label>
<select <input type="text" class="form-control" id="tenant" name="Tenant" [(ngModel)]="azure.tenant">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} companyad.onmicrosoft.com</small>
id="identityAuthority"
name="IdentityAuthority"
[(ngModel)]="azure.identityAuthority"
>
<option value="login.microsoftonline.com">Azure AD Public</option>
<option value="login.microsoftonline.us">Azure AD Government</option>
</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="tenant">{{ "tenant" | i18n }}</label> <label for="applicationId">{{'applicationId' | i18n}}</label>
<input <input type="text" class="form-control" id="applicationId" name="ApplicationId"
type="text" [(ngModel)]="azure.applicationId">
class="form-control"
id="tenant"
name="Tenant"
[(ngModel)]="azure.tenant"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} companyad.onmicrosoft.com</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="applicationId">{{ "applicationId" | i18n }}</label> <label for="secretKey">{{'secretKey' | i18n}}</label>
<input
type="text"
class="form-control"
id="applicationId"
name="ApplicationId"
[(ngModel)]="azure.applicationId"
/>
</div>
<div class="form-group">
<label for="secretKey">{{ "secretKey" | i18n }}</label>
<div class="input-group"> <div class="input-group">
<input <input type="{{showAzureKey ? 'text' : 'password'}}" class="form-control" id="secretKey" name="SecretKey"
type="{{ showAzureKey ? 'text' : 'password' }}" [(ngModel)]="azure.key">
class="form-control"
id="secretKey"
name="SecretKey"
[(ngModel)]="azure.key"
/>
<div class="input-group-append"> <div class="input-group-append">
<button <button type="button" class="btn btn-outline-secondary" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleAzureKey()">
type="button" <i class="fa fa-lg" aria-hidden="true"[ngClass]="showAzureKey ? 'fa-eye-slash' : 'fa-eye'"></i>
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleAzureKey()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showAzureKey ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button> </button>
</div> </div>
</div> </div>
@@ -309,40 +159,18 @@
</div> </div>
<div [hidden]="directory != directoryType.Okta"> <div [hidden]="directory != directoryType.Okta">
<div class="form-group"> <div class="form-group">
<label for="orgUrl">{{ "organizationUrl" | i18n }}</label> <label for="orgUrl">{{'organizationUrl' | i18n}}</label>
<input <input type="text" class="form-control" id="orgUrl" name="OrgUrl" [(ngModel)]="okta.orgUrl">
type="text" <small class="text-muted form-text">{{'ex' | i18n}} https://mycompany.okta.com/</small>
class="form-control"
id="orgUrl"
name="OrgUrl"
[(ngModel)]="okta.orgUrl"
/>
<small class="text-muted form-text"
>{{ "ex" | i18n }} https://mycompany.okta.com/</small
>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="oktaToken">{{ "token" | i18n }}</label> <label for="oktaToken">{{'token' | i18n}}</label>
<div class="input-group"> <div class="input-group">
<input <input type="{{showOktaKey ? 'text' : 'password'}}" class="form-control" id="oktaToken" name="OktaToken"
type="{{ showOktaKey ? 'text' : 'password' }}" [(ngModel)]="okta.token">
class="form-control"
id="oktaToken"
name="OktaToken"
[(ngModel)]="okta.token"
/>
<div class="input-group-append"> <div class="input-group-append">
<button <button type="button" class="btn btn-outline-secondary" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleOktaKey()">
type="button" <i class="fa fa-lg" aria-hidden="true"[ngClass]="showOktaKey ? 'fa-eye-slash' : 'fa-eye'"></i>
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleOktaKey()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showOktaKey ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button> </button>
</div> </div>
</div> </div>
@@ -350,49 +178,26 @@
</div> </div>
<div [hidden]="directory != directoryType.OneLogin"> <div [hidden]="directory != directoryType.OneLogin">
<div class="form-group"> <div class="form-group">
<label for="oneLoginClientId">{{ "clientId" | i18n }}</label> <label for="oneLoginClientId">{{'clientId' | i18n}}</label>
<input <input type="text" class="form-control" id="oneLoginClientId" name="OneLoginClientId"
type="text" [(ngModel)]="oneLogin.clientId">
class="form-control"
id="oneLoginClientId"
name="OneLoginClientId"
[(ngModel)]="oneLogin.clientId"
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="oneLoginClientSecret">{{ "clientSecret" | i18n }}</label> <label for="oneLoginClientSecret">{{'clientSecret' | i18n}}</label>
<div class="input-group"> <div class="input-group">
<input <input type="{{showOneLoginSecret ? 'text' : 'password'}}" class="form-control" id="oneLoginClientSecret" name="OneLoginClientSecret"
type="{{ showOneLoginSecret ? 'text' : 'password' }}" [(ngModel)]="oneLogin.clientSecret">
class="form-control"
id="oneLoginClientSecret"
name="OneLoginClientSecret"
[(ngModel)]="oneLogin.clientSecret"
/>
<div class="input-group-append"> <div class="input-group-append">
<button <button type="button" class="btn btn-outline-secondary" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleOneLoginSecret()">
type="button" <i class="fa fa-lg" aria-hidden="true"[ngClass]="showOneLoginSecret ? 'fa-eye-slash' : 'fa-eye'"></i>
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleOneLoginSecret()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showOneLoginSecret ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="oneLoginRegion">{{ "region" | i18n }}</label> <label for="oneLoginRegion">{{'region' | i18n}}</label>
<select <select class="form-control" id="oneLoginRegion" name="OneLoginRegion"
class="form-control" [(ngModel)]="oneLogin.region">
id="oneLoginRegion"
name="OneLoginRegion"
[(ngModel)]="oneLogin.region"
>
<option value="us">US</option> <option value="us">US</option>
<option value="eu">EU</option> <option value="eu">EU</option>
</select> </select>
@@ -400,354 +205,209 @@
</div> </div>
<div [hidden]="directory != directoryType.GSuite"> <div [hidden]="directory != directoryType.GSuite">
<div class="form-group"> <div class="form-group">
<label for="domain">{{ "domain" | i18n }}</label> <label for="domain">{{'domain' | i18n}}</label>
<input <input type="text" class="form-control" id="domain" name="Domain" [(ngModel)]="gsuite.domain">
type="text" <small class="text-muted form-text">{{'ex' | i18n}} company.com</small>
class="form-control"
id="domain"
name="Domain"
[(ngModel)]="gsuite.domain"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} company.com</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="adminUser">{{ "adminUser" | i18n }}</label> <label for="adminUser">{{'adminUser' | i18n}}</label>
<input <input type="text" class="form-control" id="adminUser" name="AdminUser"
type="text" [(ngModel)]="gsuite.adminUser">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} admin@company.com</small>
id="adminUser"
name="AdminUser"
[(ngModel)]="gsuite.adminUser"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} admin@company.com</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="customerId">{{ "customerId" | i18n }}</label> <label for="customerId">{{'customerId' | i18n}}</label>
<input <input type="text" class="form-control" id="customerId" name="CustomerId"
type="text" [(ngModel)]="gsuite.customer">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} 39204722352</small>
id="customerId"
name="CustomerId"
[(ngModel)]="gsuite.customer"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} 39204722352</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="keyFile">{{ "jsonKeyFile" | i18n }}</label> <label for="keyFile">{{'jsonKeyFile' | i18n}}</label>
<input <input type="file" class="form-control-file" id="keyFile" accept=".json"
type="file" (change)="parseKeyFile()">
class="form-control-file" <small class="text-muted form-text">{{'ex' | i18n}} My Project-jksd3jd223.json</small>
id="keyFile"
accept=".json"
(change)="parseKeyFile()"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} My Project-jksd3jd223.json</small>
</div> </div>
<div class="form-group" [hidden]="!gsuite.clientEmail"> <div class="form-group" [hidden]="!gsuite.clientEmail">
<label for="clientEmail">{{ "clientEmail" | i18n }}</label> <label for="clientEmail">{{'clientEmail' | i18n}}</label>
<input <input type="text" class="form-control" id="clientEmail" name="ClientEmail"
type="text" [(ngModel)]="gsuite.clientEmail">
class="form-control"
id="clientEmail"
name="ClientEmail"
[(ngModel)]="gsuite.clientEmail"
/>
</div> </div>
<div class="form-group" [hidden]="!gsuite.privateKey"> <div class="form-group" [hidden]="!gsuite.privateKey">
<label for="privateKey">{{ "privateKey" | i18n }}</label> <label for="privateKey">{{'privateKey' | i18n}}</label>
<textarea <textarea class="form-control text-monospace" id="privateKey" name="PrivateKey"
class="form-control text-monospace" [(ngModel)]="gsuite.privateKey">
id="privateKey"
name="PrivateKey"
[(ngModel)]="gsuite.privateKey"
>
</textarea> </textarea>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm"> <div class="col-sm">
<div class="card"> <div class="card">
<h3 class="card-header">{{ "sync" | i18n }}</h3> <h3 class="card-header">{{'sync' | i18n}}</h3>
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="interval">{{ "interval" | i18n }}</label> <label for="interval">{{'interval' | i18n}}</label>
<input <input type="number" min="5" class="form-control" id="interval" name="Interval"
type="number" [(ngModel)]="sync.interval">
min="5" <small class="text-muted form-text">{{'intervalMin' | i18n}}</small>
class="form-control"
id="interval"
name="Interval"
[(ngModel)]="sync.interval"
/>
<small class="text-muted form-text">{{ "intervalMin" | i18n }}</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="removeDisabled"
class="form-check-input" [(ngModel)]="sync.removeDisabled" name="RemoveDisabled">
type="checkbox" <label class="form-check-label" for="removeDisabled">{{'removeDisabled' | i18n}}</label>
id="removeDisabled"
[(ngModel)]="sync.removeDisabled"
name="RemoveDisabled"
/>
<label class="form-check-label" for="removeDisabled">{{
"removeDisabled" | i18n
}}</label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="overwriteExisting"
class="form-check-input" [(ngModel)]="sync.overwriteExisting" name="OverwriteExisting">
type="checkbox" <label class="form-check-label" for="overwriteExisting">{{'overwriteExisting' | i18n}}</label>
id="overwriteExisting"
[(ngModel)]="sync.overwriteExisting"
name="OverwriteExisting"
/>
<label class="form-check-label" for="overwriteExisting">{{
"overwriteExisting" | i18n
}}</label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="largeImport" [(ngModel)]="sync.largeImport"
class="form-check-input" name="LargeImport">
type="checkbox" <label class="form-check-label" for="largeImport">{{'largeImport' | i18n}}</label>
id="largeImport"
[(ngModel)]="sync.largeImport"
name="LargeImport"
/>
<label class="form-check-label" for="largeImport">{{ "largeImport" | i18n }}</label>
</div> </div>
</div> </div>
<div [hidden]="directory != directoryType.Ldap"> <div [hidden]="directory != directoryType.Ldap">
<div [hidden]="ldap.ad"> <div [hidden]="ldap.ad">
<div class="form-group"> <div class="form-group">
<label for="memberAttribute">{{ "memberAttribute" | i18n }}</label> <label for="memberAttribute">{{'memberAttribute' | i18n}}</label>
<input <input type="text" class="form-control" id="memberAttribute" name="MemberAttribute"
type="text" [(ngModel)]="sync.memberAttribute">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} uniqueMember</small>
id="memberAttribute"
name="MemberAttribute"
[(ngModel)]="sync.memberAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} uniqueMember</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="creationDateAttribute">{{ "creationDateAttribute" | i18n }}</label> <label for="creationDateAttribute">{{'creationDateAttribute' | i18n}}</label>
<input <input type="text" class="form-control" id="creationDateAttribute"
type="text" name="CreationDateAttribute" [(ngModel)]="sync.creationDateAttribute">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} whenCreated</small>
id="creationDateAttribute"
name="CreationDateAttribute"
[(ngModel)]="sync.creationDateAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} whenCreated</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="revisionDateAttribute">{{ "revisionDateAttribute" | i18n }}</label> <label for="revisionDateAttribute">{{'revisionDateAttribute' | i18n}}</label>
<input <input type="text" class="form-control" id="revisionDateAttribute"
type="text" name="RevisionDateAttribute" [(ngModel)]="sync.revisionDateAttribute">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} whenChanged</small>
id="revisionDateAttribute"
name="RevisionDateAttribute"
[(ngModel)]="sync.revisionDateAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} whenChanged</small>
</div> </div>
</div> </div>
</div> </div>
<div [hidden]="directory != directoryType.Ldap && directory != directoryType.OneLogin"> <div [hidden]="directory != directoryType.Ldap && directory != directoryType.OneLogin">
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="useEmailPrefixSuffix"
class="form-check-input" [(ngModel)]="sync.useEmailPrefixSuffix" name="UseEmailPrefixSuffix">
type="checkbox" <label class="form-check-label"
id="useEmailPrefixSuffix" for="useEmailPrefixSuffix">{{'useEmailPrefixSuffix' | i18n}}</label>
[(ngModel)]="sync.useEmailPrefixSuffix"
name="UseEmailPrefixSuffix"
/>
<label class="form-check-label" for="useEmailPrefixSuffix">{{
"useEmailPrefixSuffix" | i18n
}}</label>
</div> </div>
</div> </div>
<div [hidden]="!sync.useEmailPrefixSuffix"> <div [hidden]="!sync.useEmailPrefixSuffix">
<div class="form-group" [hidden]="ldap.ad || directory != directoryType.Ldap"> <div class="form-group" [hidden]="ldap.ad || directory != directoryType.Ldap">
<label for="emailPrefixAttribute">{{ "emailPrefixAttribute" | i18n }}</label> <label for="emailPrefixAttribute">{{'emailPrefixAttribute' | i18n}}</label>
<input <input type="text" class="form-control" id="emailPrefixAttribute"
type="text" name="EmailPrefixAttribute" [(ngModel)]="sync.emailPrefixAttribute">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} accountName</small>
id="emailPrefixAttribute"
name="EmailPrefixAttribute"
[(ngModel)]="sync.emailPrefixAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} accountName</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="emailSuffix">{{ "emailSuffix" | i18n }}</label> <label for="emailSuffix">{{'emailSuffix' | i18n}}</label>
<input <input type="text" class="form-control" id="emailSuffix" name="EmailSuffix"
type="text" [(ngModel)]="sync.emailSuffix">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} @company.com</small>
id="emailSuffix"
name="EmailSuffix"
[(ngModel)]="sync.emailSuffix"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} @company.com</small>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="syncUsers" [(ngModel)]="sync.users"
class="form-check-input" name="SyncUsers">
type="checkbox" <label class="form-check-label" for="syncUsers">{{'syncUsers' | i18n}}</label>
id="syncUsers"
[(ngModel)]="sync.users"
name="SyncUsers"
/>
<label class="form-check-label" for="syncUsers">{{ "syncUsers" | i18n }}</label>
</div> </div>
</div> </div>
<div [hidden]="!sync.users"> <div [hidden]="!sync.users">
<div class="form-group"> <div class="form-group">
<label for="userFilter">{{ "userFilter" | i18n }}</label> <label for="userFilter">{{'userFilter' | i18n}}</label>
<textarea <textarea class="form-control" id="userFilter" name="UserFilter"
class="form-control" [(ngModel)]="sync.userFilter"></textarea>
id="userFilter" <small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}}
name="UserFilter" (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))</small>
[(ngModel)]="sync.userFilter" <small class="text-muted form-text"
></textarea> *ngIf="directory === directoryType.AzureActiveDirectory">{{'ex' | i18n}}
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap" exclude:joe@company.com</small>
>{{ "ex" | i18n }} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))</small <small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}}
> exclude:joe@company.com | profile.firstName eq "John"</small>
<small <small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}}
class="text-muted form-text" exclude:joe@company.com | orgName=Engineering</small>
*ngIf="directory === directoryType.AzureActiveDirectory"
>{{ "ex" | i18n }} exclude:joe@company.com</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta"
>{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite"
>{{ "ex" | i18n }} exclude:joe@company.com | orgName=Engineering</small
>
</div> </div>
<div class="form-group" [hidden]="directory != directoryType.Ldap"> <div class="form-group" [hidden]="directory != directoryType.Ldap">
<label for="userPath">{{ "userPath" | i18n }}</label> <label for="userPath">{{'userPath' | i18n}}</label>
<input <input type="text" class="form-control" id="userPath" name="UserPath"
type="text" [(ngModel)]="sync.userPath">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} CN=Users</small>
id="userPath"
name="UserPath"
[(ngModel)]="sync.userPath"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} CN=Users</small>
</div> </div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad"> <div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="form-group"> <div class="form-group">
<label for="userObjectClass">{{ "userObjectClass" | i18n }}</label> <label for="userObjectClass">{{'userObjectClass' | i18n}}</label>
<input <input type="text" class="form-control" id="userObjectClass" name="UserObjectClass"
type="text" [(ngModel)]="sync.userObjectClass">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} inetOrgPerson</small>
id="userObjectClass"
name="UserObjectClass"
[(ngModel)]="sync.userObjectClass"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} inetOrgPerson</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="userEmailAttribute">{{ "userEmailAttribute" | i18n }}</label> <label for="userEmailAttribute">{{'userEmailAttribute' | i18n}}</label>
<input <input type="text" class="form-control" id="userEmailAttribute" name="UserEmailAttribute"
type="text" [(ngModel)]="sync.userEmailAttribute">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} mail</small>
id="userEmailAttribute"
name="UserEmailAttribute"
[(ngModel)]="sync.userEmailAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} mail</small>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="syncGroups" [(ngModel)]="sync.groups"
class="form-check-input" name="SyncGroups">
type="checkbox" <label class="form-check-label" for="syncGroups">{{(directory !== directoryType.OneLogin ?
id="syncGroups" 'syncGroups' : 'syncGroupsOneLogin') | i18n}}</label>
[(ngModel)]="sync.groups"
name="SyncGroups"
/>
<label class="form-check-label" for="syncGroups">{{
(directory !== directoryType.OneLogin ? "syncGroups" : "syncGroupsOneLogin") | i18n
}}</label>
</div> </div>
</div> </div>
<div [hidden]="!sync.groups"> <div [hidden]="!sync.groups">
<div class="form-group"> <div class="form-group">
<label for="groupFilter">{{ <label for="groupFilter">{{(directory !== directoryType.OneLogin ? 'groupFilter' :
(directory !== directoryType.OneLogin ? "groupFilter" : "groupFilterOneLogin") | i18n 'groupFilterOneLogin') | i18n}}</label>
}}</label> <textarea class="form-control" id="groupFilter" name="GroupFilter"
<textarea [(ngModel)]="sync.groupFilter"></textarea>
class="form-control" <small class="text-muted form-text" *ngIf="directory === directoryType.Ldap">{{'ex' | i18n}}
id="groupFilter" (&amp;!(name=Sales*)!(name=IT*))</small>
name="GroupFilter" <small class="text-muted form-text"
[(ngModel)]="sync.groupFilter" *ngIf="directory === directoryType.AzureActiveDirectory">{{'ex' | i18n}}
></textarea> include:Sales,IT</small>
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap" <small class="text-muted form-text" *ngIf="directory === directoryType.Okta">{{'ex' | i18n}}
>{{ "ex" | i18n }} (&amp;!(name=Sales*)!(name=IT*))</small include:Sales,IT | type eq "APP_GROUP"</small>
> <small class="text-muted form-text" *ngIf="directory === directoryType.GSuite">{{'ex' | i18n}}
<small include:Sales,IT</small>
class="text-muted form-text"
*ngIf="directory === directoryType.AzureActiveDirectory"
>{{ "ex" | i18n }} include:Sales,IT</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta"
>{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite"
>{{ "ex" | i18n }} include:Sales,IT</small
>
</div> </div>
<div class="form-group" [hidden]="directory != directoryType.Ldap"> <div class="form-group" [hidden]="directory != directoryType.Ldap">
<label for="groupPath">{{ "groupPath" | i18n }}</label> <label for="groupPath">{{'groupPath' | i18n}}</label>
<input <input type="text" class="form-control" id="groupPath" name="GroupPath"
type="text" [(ngModel)]="sync.groupPath">
class="form-control" <small class="text-muted form-text" *ngIf="!ldap.ad">{{'ex' | i18n}} CN=Groups</small>
id="groupPath" <small class="text-muted form-text" *ngIf="ldap.ad">{{'ex' | i18n}} CN=Users</small>
name="GroupPath"
[(ngModel)]="sync.groupPath"
/>
<small class="text-muted form-text" *ngIf="!ldap.ad">{{ "ex" | i18n }} CN=Groups</small>
<small class="text-muted form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} CN=Users</small>
</div> </div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad"> <div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="form-group"> <div class="form-group">
<label for="groupObjectClass">{{ "groupObjectClass" | i18n }}</label> <label for="groupObjectClass">{{'groupObjectClass' | i18n}}</label>
<input <input type="text" class="form-control" id="groupObjectClass" name="GroupObjectClass"
type="text" [(ngModel)]="sync.groupObjectClass">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} groupOfUniqueNames</small>
id="groupObjectClass"
name="GroupObjectClass"
[(ngModel)]="sync.groupObjectClass"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} groupOfUniqueNames</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="groupNameAttribute">{{ "groupNameAttribute" | i18n }}</label> <label for="groupNameAttribute">{{'groupNameAttribute' | i18n}}</label>
<input <input type="text" class="form-control" id="groupNameAttribute" name="GroupNameAttribute"
type="text" [(ngModel)]="sync.groupNameAttribute">
class="form-control" <small class="text-muted form-text">{{'ex' | i18n}} name</small>
id="groupNameAttribute"
name="GroupNameAttribute"
[(ngModel)]="sync.groupNameAttribute"
/>
<small class="text-muted form-text">{{ "ex" | i18n }} name</small>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@@ -2,20 +2,20 @@
<ul class="nav nav-tabs mb-3"> <ul class="nav nav-tabs mb-3">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="dashboard" routerLinkActive="active"> <a class="nav-link" routerLink="dashboard" routerLinkActive="active">
<i class="bwi bwi-dashboard"></i> <i class="fa fa-dashboard"></i>
{{ "dashboard" | i18n }} {{'dashboard' | i18n}}
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active"> <a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="bwi bwi-cogs"></i> <i class="fa fa-cogs"></i>
{{ "settings" | i18n }} {{'settings' | i18n}}
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="more" routerLinkActive="active"> <a class="nav-link" routerLink="more" routerLinkActive="active">
<i class="bwi bwi-sliders"></i> <i class="fa fa-sliders"></i>
{{ "more" | i18n }} {{'more' | i18n}}
</a> </a>
</li> </li>
</ul> </ul>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
src/global.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { Entry } from "./entry"; import { Entry } from './entry';
export class GroupEntry extends Entry { export class GroupEntry extends Entry {
name: string; name: string;

View File

@@ -1,6 +1,4 @@
import { IConfiguration } from "./IConfiguration"; export class GSuiteConfiguration {
export class GSuiteConfiguration implements IConfiguration {
clientEmail: string; clientEmail: string;
privateKey: string; privateKey: string;
domain: string; domain: string;

View File

@@ -1,6 +1,4 @@
import { IConfiguration } from "./IConfiguration"; export class LdapConfiguration {
export class LdapConfiguration implements IConfiguration {
ssl = false; ssl = false;
startTls = false; startTls = false;
tlsCaPath: string; tlsCaPath: string;

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { GroupEntry } from "../groupEntry"; import { GroupEntry } from '../groupEntry';
export class GroupResponse { export class GroupResponse {
externalId: string; externalId: string;

View File

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

View File

@@ -1,4 +1,4 @@
import { UserEntry } from "../userEntry"; import { UserEntry } from '../userEntry';
export class UserResponse { export class UserResponse {
externalId: string; externalId: string;

View File

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

View File

@@ -1,4 +1,4 @@
import { Entry } from "./entry"; import { Entry } from './entry';
export class UserEntry extends Entry { export class UserEntry extends Entry {
email: string; email: string;

1757
src/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ h1 {
small { small {
color: $text-muted; color: $text-muted;
font-size: $h1-font-size * 0.5; font-size: $h1-font-size * .5;
} }
} }
@@ -28,7 +28,7 @@ h4 {
} }
#duo-frame { #duo-frame {
background: url("../images/loading.svg") 0 0 no-repeat; background: url('../images/loading.svg') 0 0 no-repeat;
height: 380px; height: 380px;
iframe { iframe {
@@ -138,6 +138,7 @@ ul.testing-list {
&:focus, &:focus,
&.focus { &.focus {
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), 0.5); box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), .5);
} }
} }

View File

@@ -1,38 +1,38 @@
@import "~ngx-toastr/toastr"; $fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome.scss";
@import "~angular2-toaster/toaster";
@import "~bootstrap/scss/_variables.scss"; @import "~bootstrap/scss/_variables.scss";
.toast-container { #toast-container {
.toast-close-button { .toast-close-button {
font-size: 18px; right: -0.15em;
margin-right: 4px;
} }
.ngx-toastr { .toast {
align-items: center; opacity: 1 !important;
background-image: none !important; background-image: none !important;
border-radius: $border-radius; border-radius: $border-radius;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
display: flex; display: flex;
padding: 15px; align-items: center;
.toast-close-button {
position: absolute;
right: 5px;
top: 0;
}
&:hover { &:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
} }
.icon i::before { &:before {
float: left; font-family: FontAwesome;
font-style: normal;
font-family: $icomoon-font-family;
font-size: 25px; font-size: 25px;
line-height: 20px; line-height: 20px;
padding-right: 15px; float: left;
color: #ffffff;
padding-right: 10px;
margin: auto 0 auto -36px;
}
.toaster-icon {
display: none;
} }
.toast-message { .toast-message {
@@ -45,36 +45,40 @@
} }
} }
&.toast-danger, &.toast-danger, &.toast-error {
&.toast-error { background-image: none !important;
background-color: $danger; background-color: $danger;
.icon i::before { &:before {
content: map_get($icons, "error"); content: "\f0e7";
margin-left: -30px;
} }
} }
&.toast-warning { &.toast-warning {
background-image: none !important;
background-color: $warning; background-color: $warning;
.icon i::before { &:before {
content: map_get($icons, "exclamation-triangle"); content: "\f071";
} }
} }
&.toast-info { &.toast-info {
background-image: none !important;
background-color: $info; background-color: $info;
.icon i:before { &:before {
content: map_get($icons, "info-circle"); content: "\f05a";
} }
} }
&.toast-success { &.toast-success {
background-image: none !important;
background-color: $success; background-color: $success;
.icon i:before { &:before {
content: map_get($icons, "check"); content: "\f00C";
} }
} }
} }

View File

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

View File

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

View File

@@ -1,85 +1,51 @@
import { ApiService } from "jslib-common/abstractions/api.service"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { AppIdService } from "jslib-common/abstractions/appId.service"; import { ApiKeyService } from 'jslib-common/abstractions/apiKey.service';
import { CryptoService } from "jslib-common/abstractions/crypto.service"; import { AppIdService } from 'jslib-common/abstractions/appId.service';
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service"; import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { EnvironmentService } from "jslib-common/abstractions/environment.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { LogService } from 'jslib-common/abstractions/log.service';
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { LogService } from "jslib-common/abstractions/log.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { MessagingService } from "jslib-common/abstractions/messaging.service"; import { TokenService } from 'jslib-common/abstractions/token.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { TokenService } from "jslib-common/abstractions/token.service"; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service';
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { StateService } from "../abstractions/state.service";
import { AuthService as AuthServiceBase } from "jslib-common/services/auth.service"; import { AuthService as AuthServiceBase } from 'jslib-common/services/auth.service';
import { Account, DirectoryConfigurations, DirectorySettings } from "src/models/account"; import { AuthResult } from 'jslib-common/models/domain';
import { DeviceRequest } from 'jslib-common/models/request/deviceRequest';
import { AccountKeys, AccountProfile, AccountTokens } from "jslib-common/models/domain/account"; import { TokenRequest } from 'jslib-common/models/request/tokenRequest';
import { AuthResult } from "jslib-common/models/domain/authResult"; import { IdentityTokenResponse } from 'jslib-common/models/response/identityTokenResponse';
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 { export class AuthService extends AuthServiceBase {
constructor(
cryptoService: CryptoService, constructor(cryptoService: CryptoService, apiService: ApiService, userService: UserService,
apiService: ApiService, tokenService: TokenService, appIdService: AppIdService, i18nService: I18nService,
tokenService: TokenService, platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
appIdService: AppIdService, vaultTimeoutService: VaultTimeoutService, logService: LogService, private apiKeyService: ApiKeyService,
i18nService: I18nService, setCryptoKeys = true) {
platformUtilsService: PlatformUtilsService, super(cryptoService, apiService, userService, tokenService, appIdService, i18nService, platformUtilsService,
messagingService: MessagingService, messagingService, vaultTimeoutService, logService, setCryptoKeys);
vaultTimeoutService: VaultTimeoutService,
logService: LogService,
cryptoFunctionService: CryptoFunctionService,
environmentService: EnvironmentService,
keyConnectorService: KeyConnectorService,
stateService: StateService
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
i18nService,
platformUtilsService,
messagingService,
vaultTimeoutService,
logService,
cryptoFunctionService,
keyConnectorService,
environmentService,
stateService,
false
);
} }
async logInApiKey(clientId: string, clientSecret: string): Promise<AuthResult> { async logInApiKey(clientId: string, clientSecret: string): Promise<AuthResult> {
this.selectedTwoFactorProviderType = null; this.selectedTwoFactorProviderType = null;
if (clientId.startsWith("organization")) { if (clientId.startsWith('organization')) {
return await this.organizationLogInHelper(clientId, clientSecret); return await this.organizationLogInHelper(clientId, clientSecret);
} }
return await super.logInApiKey(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) { private async organizationLogInHelper(clientId: string, clientSecret: string) {
const appId = await this.appIdService.getAppId(); const appId = await this.appIdService.getAppId();
const entityId = clientId.split("organization.")[1];
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
const request = new TokenRequest( const request = new TokenRequest(null, null, [clientId, clientSecret], null,
null, null, false, null, deviceRequest);
null,
[clientId, clientSecret],
null,
null,
false,
null,
deviceRequest
);
const response = await this.apiService.postIdentityToken(request); const response = await this.apiService.postIdentityToken(request);
const result = new AuthResult(); const result = new AuthResult();
@@ -87,33 +53,9 @@ export class AuthService extends AuthServiceBase {
const tokenResponse = response as IdentityTokenResponse; const tokenResponse = response as IdentityTokenResponse;
result.resetMasterPassword = tokenResponse.resetMasterPassword; result.resetMasterPassword = tokenResponse.resetMasterPassword;
await this.stateService.addAccount( await this.tokenService.setToken(tokenResponse.accessToken);
new Account({ await this.apiKeyService.setInformation(clientId, clientSecret);
profile: {
...new AccountProfile(),
...{
userId: entityId,
apiKeyClientId: clientId,
entityId: entityId,
},
},
tokens: {
...new AccountTokens(),
...{
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
},
},
keys: {
...new AccountKeys(),
...{
apiKeyClientSecret: clientSecret,
},
},
directorySettings: new DirectorySettings(),
directoryConfigurations: new DirectoryConfigurations(),
})
);
return result; return result;
} }
} }

View File

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

View File

@@ -1,16 +1,16 @@
import { SyncConfiguration } from "../models/syncConfiguration"; import { SyncConfiguration } from '../models/syncConfiguration';
import { GroupEntry } from "../models/groupEntry"; import { GroupEntry } from '../models/groupEntry';
import { UserEntry } from "../models/userEntry"; import { UserEntry } from '../models/userEntry';
export abstract class BaseDirectoryService { export abstract class BaseDirectoryService {
protected createDirectoryQuery(filter: string) { protected createDirectoryQuery(filter: string) {
if (filter == null || filter === "") { if (filter == null || filter === '') {
return null; return null;
} }
const mainParts = filter.split("|"); const mainParts = filter.split('|');
if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === "") { if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === '') {
return null; return null;
} }
@@ -18,32 +18,32 @@ export abstract class BaseDirectoryService {
} }
protected createCustomSet(filter: string): [boolean, Set<string>] { protected createCustomSet(filter: string): [boolean, Set<string>] {
if (filter == null || filter === "") { if (filter == null || filter === '') {
return null; return null;
} }
const mainParts = filter.split("|"); const mainParts = filter.split('|');
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") { if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') {
return null; return null;
} }
const parts = mainParts[0].split(":"); const parts = mainParts[0].split(':');
if (parts.length !== 2) { if (parts.length !== 2) {
return null; return null;
} }
const keyword = parts[0].trim().toLowerCase(); const keyword = parts[0].trim().toLowerCase();
let exclude = true; let exclude = true;
if (keyword === "include") { if (keyword === 'include') {
exclude = false; exclude = false;
} else if (keyword === "exclude") { } else if (keyword === 'exclude') {
exclude = true; exclude = true;
} else { } else {
return null; return null;
} }
const set = new Set<string>(); const set = new Set<string>();
const pieces = parts[1].split(","); const pieces = parts[1].split(',');
for (const p of pieces) { for (const p of pieces) {
set.add(p.trim().toLowerCase()); set.add(p.trim().toLowerCase());
} }
@@ -53,7 +53,7 @@ export abstract class BaseDirectoryService {
protected filterOutResult(setFilter: [boolean, Set<string>], result: string) { protected filterOutResult(setFilter: [boolean, Set<string>], result: string) {
if (setFilter != null) { if (setFilter != null) {
const cleanResult = result != null ? result.trim().toLowerCase() : "--"; const cleanResult = result != null ? result.trim().toLowerCase() : '--';
const excluded = setFilter[0]; const excluded = setFilter[0];
const set = setFilter[1]; const set = setFilter[1];
@@ -67,17 +67,13 @@ export abstract class BaseDirectoryService {
return false; return false;
} }
protected filterUsersFromGroupsSet( protected filterUsersFromGroupsSet(users: UserEntry[], groups: GroupEntry[],
users: UserEntry[], setFilter: [boolean, Set<string>], syncConfig: SyncConfiguration): UserEntry[] {
groups: GroupEntry[],
setFilter: [boolean, Set<string>],
syncConfig: SyncConfiguration
): UserEntry[] {
if (setFilter == null || users == null) { if (setFilter == null || users == null) {
return users; return users;
} }
return users.filter((u) => { return users.filter(u => {
if (u.deleted) { if (u.deleted) {
return true; return true;
} }
@@ -85,11 +81,11 @@ export abstract class BaseDirectoryService {
return true; 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 { 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

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

View File

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

View File

@@ -1,19 +1,22 @@
import { JWT } from "google-auth-library"; import { JWT } from 'google-auth-library';
import { admin_directory_v1, google } from "googleapis"; import {
admin_directory_v1,
google,
} from 'googleapis';
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from '../enums/directoryType';
import { GroupEntry } from "../models/groupEntry"; import { GroupEntry } from '../models/groupEntry';
import { GSuiteConfiguration } from "../models/gsuiteConfiguration"; import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
import { SyncConfiguration } from "../models/syncConfiguration"; import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from "../models/userEntry"; import { UserEntry } from '../models/userEntry';
import { BaseDirectoryService } from "./baseDirectory.service"; import { BaseDirectoryService } from './baseDirectory.service';
import { IDirectoryService } from "./directory.service"; import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from "jslib-common/abstractions/log.service"; import { LogService } from 'jslib-common/abstractions/log.service';
import { StateService } from "../abstractions/state.service";
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService { export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
private client: JWT; private client: JWT;
@@ -22,29 +25,24 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
private dirConfig: GSuiteConfiguration; private dirConfig: GSuiteConfiguration;
private syncConfig: SyncConfiguration; private syncConfig: SyncConfiguration;
constructor( constructor(private configurationService: ConfigurationService, private logService: LogService,
private logService: LogService, private i18nService: I18nService) {
private i18nService: I18nService,
private stateService: StateService
) {
super(); super();
this.service = google.admin("directory_v1"); this.service = google.admin('directory_v1');
} }
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.stateService.getDirectoryType(); const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.GSuite) { if (type !== DirectoryType.GSuite) {
return; return;
} }
this.dirConfig = await this.stateService.getDirectory<GSuiteConfiguration>( this.dirConfig = await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite);
DirectoryType.GSuite
);
if (this.dirConfig == null) { if (this.dirConfig == null) {
return; return;
} }
this.syncConfig = await this.stateService.getSync(); this.syncConfig = await this.configurationService.getSync();
if (this.syncConfig == null) { if (this.syncConfig == null) {
return; return;
} }
@@ -73,11 +71,11 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
const filter = this.createCustomSet(this.syncConfig.userFilter); const filter = this.createCustomSet(this.syncConfig.userFilter);
while (true) { while (true) {
this.logService.info("Querying users - nextPageToken:" + nextPageToken); this.logService.info('Querying users - nextPageToken:' + nextPageToken);
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams); const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
const res = await this.service.users.list(p); const res = await this.service.users.list(p);
if (res.status !== 200) { if (res.status !== 200) {
throw new Error("User list API failed: " + res.statusText); throw new Error('User list API failed: ' + res.statusText);
} }
nextPageToken = res.data.nextPageToken; nextPageToken = res.data.nextPageToken;
@@ -100,14 +98,11 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
nextPageToken = null; nextPageToken = null;
while (true) { while (true) {
this.logService.info("Querying deleted users - nextPageToken:" + nextPageToken); this.logService.info('Querying deleted users - nextPageToken:' + nextPageToken);
const p = Object.assign( const p = Object.assign({ showDeleted: true, query: query, pageToken: nextPageToken }, this.authParams);
{ showDeleted: true, query: query, pageToken: nextPageToken },
this.authParams
);
const delRes = await this.service.users.list(p); const delRes = await this.service.users.list(p);
if (delRes.status !== 200) { if (delRes.status !== 200) {
throw new Error("Deleted user list API failed: " + delRes.statusText); throw new Error('Deleted user list API failed: ' + delRes.statusText);
} }
nextPageToken = delRes.data.nextPageToken; nextPageToken = delRes.data.nextPageToken;
@@ -132,7 +127,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
} }
private buildUser(user: admin_directory_v1.Schema$User, deleted: boolean) { private buildUser(user: admin_directory_v1.Schema$User, deleted: boolean) {
if ((user.emails == null || user.emails === "") && !deleted) { if ((user.emails == null || user.emails === '') && !deleted) {
return null; return null;
} }
@@ -145,19 +140,16 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
return entry; return entry;
} }
private async getGroups( private async getGroups(setFilter: [boolean, Set<string>], users: UserEntry[]): Promise<GroupEntry[]> {
setFilter: [boolean, Set<string>],
users: UserEntry[]
): Promise<GroupEntry[]> {
const entries: GroupEntry[] = []; const entries: GroupEntry[] = [];
let nextPageToken: string = null; let nextPageToken: string = null;
while (true) { while (true) {
this.logService.info("Querying groups - nextPageToken:" + nextPageToken); this.logService.info('Querying groups - nextPageToken:' + nextPageToken);
const p = Object.assign({ pageToken: nextPageToken }, this.authParams); const p = Object.assign({ pageToken: nextPageToken }, this.authParams);
const res = await this.service.groups.list(p); const res = await this.service.groups.list(p);
if (res.status !== 200) { if (res.status !== 200) {
throw new Error("Group list API failed: " + res.statusText); throw new Error('Group list API failed: ' + res.statusText);
} }
nextPageToken = res.data.nextPageToken; nextPageToken = res.data.nextPageToken;
@@ -190,7 +182,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams); const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
const memRes = await this.service.members.list(p); const memRes = await this.service.members.list(p);
if (memRes.status !== 200) { if (memRes.status !== 200) {
this.logService.warning("Group member list API failed: " + memRes.statusText); this.logService.warning('Group member list API failed: ' + memRes.statusText);
return entry; return entry;
} }
@@ -201,14 +193,14 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
continue; continue;
} }
const type = member.type.toLowerCase(); const type = member.type.toLowerCase();
if (type === "user") { if (type === 'user') {
if (member.status == null || member.status.toLowerCase() !== "active") { if (member.status == null || member.status.toLowerCase() !== 'active') {
continue; continue;
} }
entry.userMemberExternalIds.add(member.id); entry.userMemberExternalIds.add(member.id);
} else if (type === "group") { } else if (type === 'group') {
entry.groupMemberReferenceIds.add(member.id); entry.groupMemberReferenceIds.add(member.id);
} else if (type === "customer") { } else if (type === 'customer') {
for (const user of users) { for (const user of users) {
entry.userMemberExternalIds.add(user.externalId); entry.userMemberExternalIds.add(user.externalId);
} }
@@ -225,13 +217,9 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
} }
private async auth() { private async auth() {
if ( if (this.dirConfig.clientEmail == null || this.dirConfig.privateKey == null ||
this.dirConfig.clientEmail == null || this.dirConfig.adminUser == null || this.dirConfig.domain == null) {
this.dirConfig.privateKey == null || throw new Error(this.i18nService.t('dirConfigIncomplete'));
this.dirConfig.adminUser == null ||
this.dirConfig.domain == null
) {
throw new Error(this.i18nService.t("dirConfigIncomplete"));
} }
this.client = new google.auth.JWT({ this.client = new google.auth.JWT({
@@ -239,9 +227,9 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null, key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null,
subject: this.dirConfig.adminUser, subject: this.dirConfig.adminUser,
scopes: [ scopes: [
"https://www.googleapis.com/auth/admin.directory.user.readonly", 'https://www.googleapis.com/auth/admin.directory.user.readonly',
"https://www.googleapis.com/auth/admin.directory.group.readonly", 'https://www.googleapis.com/auth/admin.directory.group.readonly',
"https://www.googleapis.com/auth/admin.directory.group.member.readonly", 'https://www.googleapis.com/auth/admin.directory.group.member.readonly',
], ],
}); });
@@ -250,10 +238,10 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
this.authParams = { this.authParams = {
auth: this.client, auth: this.client,
}; };
if (this.dirConfig.domain != null && this.dirConfig.domain.trim() !== "") { if (this.dirConfig.domain != null && this.dirConfig.domain.trim() !== '') {
this.authParams.domain = this.dirConfig.domain; this.authParams.domain = this.dirConfig.domain;
} }
if (this.dirConfig.customer != null && this.dirConfig.customer.trim() !== "") { if (this.dirConfig.customer != null && this.dirConfig.customer.trim() !== '') {
this.authParams.customer = this.dirConfig.customer; this.authParams.customer = this.dirConfig.customer;
} }
} }

View File

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

View File

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

View File

@@ -1,22 +1,22 @@
import * as fs from "fs"; import * as fs from 'fs';
import * as ldap from "ldapjs"; import * as ldap from 'ldapjs';
import { checkServerIdentity, PeerCertificate } from "tls"; import { checkServerIdentity, PeerCertificate } from 'tls';
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from '../enums/directoryType';
import { GroupEntry } from "../models/groupEntry"; import { GroupEntry } from '../models/groupEntry';
import { LdapConfiguration } from "../models/ldapConfiguration"; import { LdapConfiguration } from '../models/ldapConfiguration';
import { SyncConfiguration } from "../models/syncConfiguration"; import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from "../models/userEntry"; import { UserEntry } from '../models/userEntry';
import { IDirectoryService } from "./directory.service"; import { ConfigurationService } from './configuration.service';
import { IDirectoryService } from './directory.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from "jslib-common/abstractions/log.service"; import { LogService } from 'jslib-common/abstractions/log.service';
import { StateService } from "../abstractions/state.service";
import { Utils } from "jslib-common/misc/utils"; import { Utils } from 'jslib-common/misc/utils';
const UserControlAccountDisabled = 2; const UserControlAccountDisabled = 2;
@@ -25,24 +25,21 @@ export class LdapDirectoryService implements IDirectoryService {
private dirConfig: LdapConfiguration; private dirConfig: LdapConfiguration;
private syncConfig: SyncConfiguration; private syncConfig: SyncConfiguration;
constructor( constructor(private configurationService: ConfigurationService, private logService: LogService,
private logService: LogService, private i18nService: I18nService) { }
private i18nService: I18nService,
private stateService: StateService
) {}
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.stateService.getDirectoryType(); const type = await this.configurationService.getDirectoryType();
if (type !== DirectoryType.Ldap) { if (type !== DirectoryType.Ldap) {
return; return;
} }
this.dirConfig = await this.stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap); this.dirConfig = await this.configurationService.getDirectory<LdapConfiguration>(DirectoryType.Ldap);
if (this.dirConfig == null) { if (this.dirConfig == null) {
return; return;
} }
this.syncConfig = await this.stateService.getSync(); this.syncConfig = await this.configurationService.getSync();
if (this.syncConfig == null) { if (this.syncConfig == null) {
return; return;
} }
@@ -58,7 +55,7 @@ export class LdapDirectoryService implements IDirectoryService {
if (this.syncConfig.groups) { if (this.syncConfig.groups) {
let groupForce = force; let groupForce = force;
if (!groupForce && users != null) { 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; groupForce = activeUsers.length > 0;
} }
groups = await this.getGroups(groupForce); groups = await this.getGroups(groupForce);
@@ -69,40 +66,31 @@ export class LdapDirectoryService implements IDirectoryService {
} }
private async getUsers(force: boolean): Promise<UserEntry[]> { private async getUsers(force: boolean): Promise<UserEntry[]> {
const lastSync = await this.stateService.getLastUserSync(); const lastSync = await this.configurationService.getLastUserSyncDate();
let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter); let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter);
filter = this.buildRevisionFilter(filter, force, lastSync); filter = this.buildRevisionFilter(filter, force, lastSync);
const path = this.makeSearchPath(this.syncConfig.userPath); const path = this.makeSearchPath(this.syncConfig.userPath);
this.logService.info("User search: " + path + " => " + filter); this.logService.info('User search: ' + path + ' => ' + filter);
const regularUsers = await this.search<UserEntry>(path, filter, (se: any) => const regularUsers = await this.search<UserEntry>(path, filter, (se: any) => this.buildUser(se, false));
this.buildUser(se, false)
);
if (!this.dirConfig.ad) { if (!this.dirConfig.ad) {
return regularUsers; return regularUsers;
} }
try { try {
let deletedFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, "(isDeleted=TRUE)"); let deletedFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, '(isDeleted=TRUE)');
deletedFilter = this.buildRevisionFilter(deletedFilter, force, lastSync); deletedFilter = this.buildRevisionFilter(deletedFilter, force, lastSync);
const deletedPath = this.makeSearchPath("CN=Deleted Objects"); const deletedPath = this.makeSearchPath('CN=Deleted Objects');
this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter); this.logService.info('Deleted user search: ' + deletedPath + ' => ' + deletedFilter);
const delControl = new (ldap as any).Control({ const delControl = new (ldap as any).Control({ type: '1.2.840.113556.1.4.417', criticality: true });
type: "1.2.840.113556.1.4.417", const deletedUsers = await this.search<UserEntry>(deletedPath, deletedFilter,
criticality: true, (se: any) => this.buildUser(se, true), [delControl]);
});
const deletedUsers = await this.search<UserEntry>(
deletedPath,
deletedFilter,
(se: any) => this.buildUser(se, true),
[delControl]
);
return regularUsers.concat(deletedUsers); return regularUsers.concat(deletedUsers);
} catch (e) { } catch (e) {
this.logService.warning("Cannot query deleted users."); this.logService.warning('Cannot query deleted users.');
return regularUsers; return regularUsers;
} }
} }
@@ -119,12 +107,8 @@ export class LdapDirectoryService implements IDirectoryService {
user.externalId = this.getExternalId(searchEntry, user.referenceId); user.externalId = this.getExternalId(searchEntry, user.referenceId);
user.disabled = this.entryDisabled(searchEntry); user.disabled = this.entryDisabled(searchEntry);
user.email = this.getAttr(searchEntry, this.syncConfig.userEmailAttribute); user.email = this.getAttr(searchEntry, this.syncConfig.userEmailAttribute);
if ( if (user.email == null && this.syncConfig.useEmailPrefixSuffix &&
user.email == null && this.syncConfig.emailPrefixAttribute != null && this.syncConfig.emailSuffix != null) {
this.syncConfig.useEmailPrefixSuffix &&
this.syncConfig.emailPrefixAttribute != null &&
this.syncConfig.emailSuffix != null
) {
const prefixAttr = this.getAttr(searchEntry, this.syncConfig.emailPrefixAttribute); const prefixAttr = this.getAttr(searchEntry, this.syncConfig.emailPrefixAttribute);
if (prefixAttr != null) { if (prefixAttr != null) {
user.email = prefixAttr + this.syncConfig.emailSuffix; user.email = prefixAttr + this.syncConfig.emailSuffix;
@@ -135,7 +119,7 @@ export class LdapDirectoryService implements IDirectoryService {
user.email = user.email.trim().toLowerCase(); user.email = user.email.trim().toLowerCase();
} }
if (!user.deleted && (user.email == null || user.email.trim() === "")) { if (!user.deleted && (user.email == null || user.email.trim() === '')) {
return null; return null;
} }
@@ -145,18 +129,15 @@ export class LdapDirectoryService implements IDirectoryService {
private async getGroups(force: boolean): Promise<GroupEntry[]> { private async getGroups(force: boolean): Promise<GroupEntry[]> {
const entries: GroupEntry[] = []; const entries: GroupEntry[] = [];
const lastSync = await this.stateService.getLastUserSync(); const lastSync = await this.configurationService.getLastUserSyncDate();
const originalFilter = this.buildBaseFilter( const originalFilter = this.buildBaseFilter(this.syncConfig.groupObjectClass, this.syncConfig.groupFilter);
this.syncConfig.groupObjectClass,
this.syncConfig.groupFilter
);
let filter = originalFilter; let filter = originalFilter;
const revisionFilter = this.buildRevisionFilter(filter, force, lastSync); const revisionFilter = this.buildRevisionFilter(filter, force, lastSync);
const searchSinceRevision = filter !== revisionFilter; const searchSinceRevision = filter !== revisionFilter;
filter = revisionFilter; filter = revisionFilter;
const path = this.makeSearchPath(this.syncConfig.groupPath); const path = this.makeSearchPath(this.syncConfig.groupPath);
this.logService.info("Group search: " + path + " => " + filter); this.logService.info('Group search: ' + path + ' => ' + filter);
let groupSearchEntries: any[] = []; let groupSearchEntries: any[] = [];
const initialSearchGroupIds = await this.search<string>(path, filter, (se: any) => { const initialSearchGroupIds = await this.search<string>(path, filter, (se: any) => {
@@ -170,10 +151,7 @@ export class LdapDirectoryService implements IDirectoryService {
groupSearchEntries = await this.search<string>(path, originalFilter, (se: any) => se); groupSearchEntries = await this.search<string>(path, originalFilter, (se: any) => se);
} }
const userFilter = this.buildBaseFilter( const userFilter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter);
this.syncConfig.userObjectClass,
this.syncConfig.userFilter
);
const userPath = this.makeSearchPath(this.syncConfig.userPath); const userPath = this.makeSearchPath(this.syncConfig.userPath);
const userIdMap = new Map<string, string>(); const userIdMap = new Map<string, string>();
await this.search<string>(userPath, userFilter, (se: any) => { await this.search<string>(userPath, userFilter, (se: any) => {
@@ -202,7 +180,7 @@ export class LdapDirectoryService implements IDirectoryService {
group.name = this.getAttr(searchEntry, this.syncConfig.groupNameAttribute); group.name = this.getAttr(searchEntry, this.syncConfig.groupNameAttribute);
if (group.name == null) { if (group.name == null) {
group.name = this.getAttr(searchEntry, "cn"); group.name = this.getAttr(searchEntry, 'cn');
} }
if (group.name == null) { if (group.name == null) {
@@ -224,7 +202,7 @@ export class LdapDirectoryService implements IDirectoryService {
} }
private getExternalId(searchEntry: any, referenceId: string) { private getExternalId(searchEntry: any, referenceId: string) {
const attrObj = this.getAttrObj(searchEntry, "objectGUID"); const attrObj = this.getAttrObj(searchEntry, 'objectGUID');
if (attrObj != null && attrObj._vals != null && attrObj._vals.length > 0) { if (attrObj != null && attrObj._vals != null && attrObj._vals.length > 0) {
return this.bufToGuid(attrObj._vals[0]); return this.bufToGuid(attrObj._vals[0]);
} else { } else {
@@ -234,35 +212,35 @@ export class LdapDirectoryService implements IDirectoryService {
private buildBaseFilter(objectClass: string, subFilter: string): string { private buildBaseFilter(objectClass: string, subFilter: string): string {
let filter = this.buildObjectClassFilter(objectClass); let filter = this.buildObjectClassFilter(objectClass);
if (subFilter != null && subFilter.trim() !== "") { if (subFilter != null && subFilter.trim() !== '') {
filter = "(&" + filter + subFilter + ")"; filter = '(&' + filter + subFilter + ')';
} }
return filter; return filter;
} }
private buildObjectClassFilter(objectClass: string): string { private buildObjectClassFilter(objectClass: string): string {
return "(&(objectClass=" + objectClass + "))"; return '(&(objectClass=' + objectClass + '))';
} }
private buildRevisionFilter(baseFilter: string, force: boolean, lastRevisionDate: Date) { private buildRevisionFilter(baseFilter: string, force: boolean, lastRevisionDate: Date) {
const revisionAttr = this.syncConfig.revisionDateAttribute; const revisionAttr = this.syncConfig.revisionDateAttribute;
if (!force && lastRevisionDate != null && revisionAttr != null && revisionAttr.trim() !== "") { if (!force && lastRevisionDate != null && revisionAttr != null && revisionAttr.trim() !== '') {
const dateString = lastRevisionDate.toISOString().replace(/[-:T]/g, "").substr(0, 16) + "Z"; const dateString = lastRevisionDate.toISOString().replace(/[-:T]/g, '').substr(0, 16) + 'Z';
baseFilter = "(&" + baseFilter + "(" + revisionAttr + ">=" + dateString + "))"; baseFilter = '(&' + baseFilter + '(' + revisionAttr + '>=' + dateString + '))';
} }
return baseFilter; return baseFilter;
} }
private makeSearchPath(pathPrefix: string) { private makeSearchPath(pathPrefix: string) {
if (this.dirConfig.rootPath.toLowerCase().indexOf("dc=") === -1) { if (this.dirConfig.rootPath.toLowerCase().indexOf('dc=') === -1) {
return pathPrefix; return pathPrefix;
} }
if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== "") { if (this.dirConfig.rootPath != null && this.dirConfig.rootPath.trim() !== '') {
const trimmedRootPath = this.dirConfig.rootPath.trim().toLowerCase(); const trimmedRootPath = this.dirConfig.rootPath.trim().toLowerCase();
let path = trimmedRootPath.substr(trimmedRootPath.indexOf("dc=")); let path = trimmedRootPath.substr(trimmedRootPath.indexOf('dc='));
if (pathPrefix != null && pathPrefix.trim() !== "") { if (pathPrefix != null && pathPrefix.trim() !== '') {
path = pathPrefix.trim() + "," + path; path = pathPrefix.trim() + ',' + path;
} }
return path; return path;
} }
@@ -276,12 +254,7 @@ export class LdapDirectoryService implements IDirectoryService {
} }
const attrs = searchEntry.attributes.filter((a: any) => a.type === attr); const attrs = searchEntry.attributes.filter((a: any) => a.type === attr);
if ( if (attrs == null || attrs.length === 0 || attrs[0].vals == null || attrs[0].vals.length === 0) {
attrs == null ||
attrs.length === 0 ||
attrs[0].vals == null ||
attrs[0].vals.length === 0
) {
return null; return null;
} }
@@ -305,29 +278,23 @@ export class LdapDirectoryService implements IDirectoryService {
} }
private entryDisabled(searchEntry: any): boolean { private entryDisabled(searchEntry: any): boolean {
const c = this.getAttr(searchEntry, "userAccountControl"); const c = this.getAttr(searchEntry, 'userAccountControl');
if (c != null) { if (c != null) {
try { try {
const control = parseInt(c, null); const control = parseInt(c, null);
// tslint:disable-next-line // tslint:disable-next-line
return (control & UserControlAccountDisabled) === UserControlAccountDisabled; return (control & UserControlAccountDisabled) === UserControlAccountDisabled;
} catch (e) { } catch { }
this.logService.error(e);
}
} }
return false; return false;
} }
private async search<T>( private async search<T>(path: string, filter: string, processEntry: (searchEntry: any) => T,
path: string, controls: ldap.Control[] = []): Promise<T[]> {
filter: string,
processEntry: (searchEntry: any) => T,
controls: ldap.Control[] = []
): Promise<T[]> {
const options: ldap.SearchOptions = { const options: ldap.SearchOptions = {
filter: filter, filter: filter,
scope: "sub", scope: 'sub',
paged: this.dirConfig.pagedSearch, paged: this.dirConfig.pagedSearch,
}; };
const entries: T[] = []; const entries: T[] = [];
@@ -338,18 +305,18 @@ export class LdapDirectoryService implements IDirectoryService {
return; return;
} }
res.on("error", (resErr) => { res.on('error', resErr => {
reject(resErr); reject(resErr);
}); });
res.on("searchEntry", (entry) => { res.on('searchEntry', entry => {
const e = processEntry(entry); const e = processEntry(entry);
if (e != null) { if (e != null) {
entries.push(e); entries.push(e);
} }
}); });
res.on("end", (result) => { res.on('end', result => {
resolve(entries); resolve(entries);
}); });
}); });
@@ -359,11 +326,12 @@ export class LdapDirectoryService implements IDirectoryService {
private async bind(): Promise<any> { private async bind(): Promise<any> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if (this.dirConfig.hostname == null || this.dirConfig.port == null) { if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
reject(this.i18nService.t("dirConfigIncomplete")); reject(this.i18nService.t('dirConfigIncomplete'));
return; return;
} }
const protocol = "ldap" + (this.dirConfig.ssl && !this.dirConfig.startTls ? "s" : ""); const protocol = 'ldap' + (this.dirConfig.ssl && !this.dirConfig.startTls ? 's' : '');
const url = protocol + "://" + this.dirConfig.hostname + ":" + this.dirConfig.port; const url = protocol + '://' + this.dirConfig.hostname +
':' + this.dirConfig.port;
const options: ldap.ClientOptions = { const options: ldap.ClientOptions = {
url: url.trim().toLowerCase(), url: url.trim().toLowerCase(),
}; };
@@ -374,33 +342,21 @@ export class LdapDirectoryService implements IDirectoryService {
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized; tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
} }
if (!this.dirConfig.startTls) { if (!this.dirConfig.startTls) {
if ( if (this.dirConfig.sslCaPath != null && this.dirConfig.sslCaPath !== '' &&
this.dirConfig.sslCaPath != null && fs.existsSync(this.dirConfig.sslCaPath)) {
this.dirConfig.sslCaPath !== "" &&
fs.existsSync(this.dirConfig.sslCaPath)
) {
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)]; tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
} }
if ( if (this.dirConfig.sslCertPath != null && this.dirConfig.sslCertPath !== '' &&
this.dirConfig.sslCertPath != null && fs.existsSync(this.dirConfig.sslCertPath)) {
this.dirConfig.sslCertPath !== "" &&
fs.existsSync(this.dirConfig.sslCertPath)
) {
tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath); tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath);
} }
if ( if (this.dirConfig.sslKeyPath != null && this.dirConfig.sslKeyPath !== '' &&
this.dirConfig.sslKeyPath != null && fs.existsSync(this.dirConfig.sslKeyPath)) {
this.dirConfig.sslKeyPath !== "" &&
fs.existsSync(this.dirConfig.sslKeyPath)
) {
tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath); tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
} }
} else { } else {
if ( if (this.dirConfig.tlsCaPath != null && this.dirConfig.tlsCaPath !== '' &&
this.dirConfig.tlsCaPath != null && fs.existsSync(this.dirConfig.tlsCaPath)) {
this.dirConfig.tlsCaPath !== "" &&
fs.existsSync(this.dirConfig.tlsCaPath)
) {
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)]; tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
} }
} }
@@ -411,17 +367,13 @@ export class LdapDirectoryService implements IDirectoryService {
this.client = ldap.createClient(options); this.client = ldap.createClient(options);
const user = const user = this.dirConfig.username == null || this.dirConfig.username.trim() === '' ? null :
this.dirConfig.username == null || this.dirConfig.username.trim() === "" this.dirConfig.username;
? null const pass = this.dirConfig.password == null || this.dirConfig.password.trim() === '' ? null :
: this.dirConfig.username; this.dirConfig.password;
const pass =
this.dirConfig.password == null || this.dirConfig.password.trim() === ""
? null
: this.dirConfig.password;
if (user == null || pass == null) { if (user == null || pass == null) {
reject(this.i18nService.t("usernamePasswordNotConfigured")); reject(this.i18nService.t('usernamePasswordNotConfigured'));
return; return;
} }
@@ -430,7 +382,7 @@ export class LdapDirectoryService implements IDirectoryService {
if (err != null) { if (err != null) {
reject(err.message); reject(err.message);
} else { } else {
this.client.bind(user, pass, (err2) => { this.client.bind(user, pass, err2 => {
if (err2 != null) { if (err2 != null) {
reject(err2.message); reject(err2.message);
} else { } else {
@@ -440,7 +392,7 @@ export class LdapDirectoryService implements IDirectoryService {
} }
}); });
} else { } else {
this.client.bind(user, pass, (err) => { this.client.bind(user, pass, err => {
if (err != null) { if (err != null) {
reject(err.message); reject(err.message);
} else { } else {
@@ -453,7 +405,7 @@ export class LdapDirectoryService implements IDirectoryService {
private async unbind(): Promise<void> { private async unbind(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.client.unbind((err) => { this.client.unbind(err => {
if (err != null) { if (err != null) {
reject(err); reject(err);
} else { } else {
@@ -470,16 +422,8 @@ export class LdapDirectoryService implements IDirectoryService {
const p3 = arr.slice(6, 8).reverse().buffer; const p3 = arr.slice(6, 8).reverse().buffer;
const p4 = arr.slice(8, 10).buffer; const p4 = arr.slice(8, 10).buffer;
const p5 = arr.slice(10).buffer; const p5 = arr.slice(10).buffer;
const guid = const guid = Utils.fromBufferToHex(p1) + '-' + Utils.fromBufferToHex(p2) + '-' + Utils.fromBufferToHex(p3) +
Utils.fromBufferToHex(p1) + '-' + Utils.fromBufferToHex(p4) + '-' + Utils.fromBufferToHex(p5);
"-" +
Utils.fromBufferToHex(p2) +
"-" +
Utils.fromBufferToHex(p3) +
"-" +
Utils.fromBufferToHex(p4) +
"-" +
Utils.fromBufferToHex(p5);
return guid.toLowerCase(); return guid.toLowerCase();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,15 @@
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { SyncService } from "./services/sync.service"; import { SyncService } from './services/sync.service';
import { Entry } from "./models/entry"; import { Entry } from './models/entry';
import { LdapConfiguration } from "./models/ldapConfiguration"; import { LdapConfiguration } from './models/ldapConfiguration';
import { SimResult } from "./models/simResult"; import { SimResult } from './models/simResult';
import { SyncConfiguration } from "./models/syncConfiguration"; import { SyncConfiguration } from './models/syncConfiguration';
import { UserEntry } from "./models/userEntry"; import { UserEntry } from './models/userEntry';
export class ConnectorUtils { export class ConnectorUtils {
static async simulate( static async simulate(syncService: SyncService, i18nService: I18nService, sinceLast: boolean): Promise<SimResult> {
syncService: SyncService,
i18nService: I18nService,
sinceLast: boolean
): Promise<SimResult> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const simResult = new SimResult(); const simResult = new SimResult();
try { try {
@@ -27,7 +23,7 @@ export class ConnectorUtils {
} catch (e) { } catch (e) {
simResult.groups = null; simResult.groups = null;
simResult.users = null; simResult.users = null;
reject(e || i18nService.t("syncError")); reject(e || i18nService.t('syncError'));
return; return;
} }
@@ -50,7 +46,7 @@ export class ConnectorUtils {
continue; continue;
} }
const anyG = g as any; const anyG = (g as any);
anyG.users = []; anyG.users = [];
for (const uid of g.userMemberExternalIds) { for (const uid of g.userMemberExternalIds) {
if (userMap.has(uid)) { if (userMap.has(uid)) {
@@ -69,20 +65,20 @@ export class ConnectorUtils {
static adjustConfigForSave(ldap: LdapConfiguration, sync: SyncConfiguration) { static adjustConfigForSave(ldap: LdapConfiguration, sync: SyncConfiguration) {
if (ldap.ad) { if (ldap.ad) {
sync.creationDateAttribute = "whenCreated"; sync.creationDateAttribute = 'whenCreated';
sync.revisionDateAttribute = "whenChanged"; sync.revisionDateAttribute = 'whenChanged';
sync.emailPrefixAttribute = "sAMAccountName"; sync.emailPrefixAttribute = 'sAMAccountName';
sync.memberAttribute = "member"; sync.memberAttribute = 'member';
sync.userObjectClass = "person"; sync.userObjectClass = 'person';
sync.groupObjectClass = "group"; sync.groupObjectClass = 'group';
sync.userEmailAttribute = "mail"; sync.userEmailAttribute = 'mail';
sync.groupNameAttribute = "name"; sync.groupNameAttribute = 'name';
if (sync.groupPath == null) { if (sync.groupPath == null) {
sync.groupPath = "CN=Users"; sync.groupPath = 'CN=Users';
} }
if (sync.userPath == null) { if (sync.userPath == null) {
sync.userPath = "CN=Users"; sync.userPath = 'CN=Users';
} }
} }
@@ -97,9 +93,8 @@ export class ConnectorUtils {
private static sortEntries(arr: Entry[], i18nService: I18nService) { private static sortEntries(arr: Entry[], i18nService: I18nService) {
arr.sort((a, b) => { arr.sort((a, b) => {
return i18nService.collator return i18nService.collator ? i18nService.collator.compare(a.displayName, b.displayName) :
? i18nService.collator.compare(a.displayName, b.displayName) a.displayName.localeCompare(b.displayName);
: a.displayName.localeCompare(b.displayName);
}); });
} }
} }

View File

@@ -12,15 +12,28 @@
"types": [], "types": [],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"tldjs": ["jslib/src/misc/tldjs.noop"], "tldjs": [
"jslib-common/*": ["jslib/common/src/*"], "jslib/src/misc/tldjs.noop"
"jslib-angular/*": ["jslib/angular/src/*"], ],
"jslib-electron/*": ["jslib/electron/src/*"], "jslib-common/*": [
"jslib-node/*": ["jslib/node/src/*"] "jslib/common/src/*"
],
"jslib-angular/*": [
"jslib/angular/src/*"
],
"jslib-electron/*": [
"jslib/electron/src/*"
],
"jslib-node/*": [
"jslib/node/src/*"
]
} }
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"preserveWhitespaces": true "preserveWhitespaces": true
}, },
"include": ["src", "src-cli"] "include": [
"src",
"src-cli"
]
} }

View File

@@ -1,17 +1,17 @@
{ {
"extends": "tslint:recommended", "extends": "tslint:recommended",
"rules": { "rules": {
"align": [true, "statements", "members"], "align": [ true, "statements", "members" ],
"ban-types": { "ban-types": {
"options": [ "options": [
["Object", "Avoid using the `Object` type. Did you mean `object`?"], [ "Object", "Avoid using the `Object` type. Did you mean `object`?" ],
["Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?"], [ "Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?" ],
["Number", "Avoid using the `Number` type. Did you mean `number`?"], [ "Number", "Avoid using the `Number` type. Did you mean `number`?" ],
["String", "Avoid using the `String` type. Did you mean `string`?"], [ "String", "Avoid using the `String` type. Did you mean `string`?" ],
["Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?"] [ "Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?" ]
] ]
}, },
"member-access": [true, "no-public"], "member-access": [ true, "no-public" ],
"member-ordering": [ "member-ordering": [
true, true,
{ {
@@ -34,10 +34,11 @@
] ]
} }
], ],
"no-empty": [true], "no-empty": [ true, "allow-empty-catch" ],
"object-literal-sort-keys": false, "object-literal-sort-keys": false,
"object-literal-shorthand": [true, "never"], "object-literal-shorthand": [ true, "never" ],
"prefer-for-of": false, "prefer-for-of": false,
"quotemark": [ true, "single" ],
"whitespace": [ "whitespace": [
true, true,
"check-branch", "check-branch",
@@ -50,7 +51,14 @@
], ],
"max-classes-per-file": false, "max-classes-per-file": false,
"ordered-imports": true, "ordered-imports": true,
"arrow-parens": [true], "arrow-parens": [
true,
"ban-single-arg-parens"
],
"semicolon": [
true,
"always"
],
"trailing-comma": [ "trailing-comma": [
true, true,
{ {

1
webfonts.list Normal file
View File

@@ -0,0 +1 @@
Open+Sans:300,300i,400,400i,600,600i,700,700i,800,800i&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext

View File

@@ -1,73 +1,72 @@
const path = require("path"); const path = require('path');
const webpack = require("webpack"); const webpack = require('webpack');
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require('copy-webpack-plugin');
const nodeExternals = require("webpack-node-externals"); const nodeExternals = require('webpack-node-externals');
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
if (process.env.NODE_ENV == null) { if (process.env.NODE_ENV == null) {
process.env.NODE_ENV = "development"; process.env.NODE_ENV = 'development';
} }
const ENV = (process.env.ENV = process.env.NODE_ENV); const ENV = process.env.ENV = process.env.NODE_ENV;
const moduleRules = [ const moduleRules = [
{ {
test: /\.ts$/, test: /\.ts$/,
enforce: "pre", enforce: 'pre',
loader: "tslint-loader", loader: 'tslint-loader',
}, },
{ {
test: /\.ts$/, test: /\.ts$/,
use: "ts-loader", loaders: ['ts-loader'],
exclude: path.resolve(__dirname, "node_modules"), exclude: path.resolve(__dirname, 'node_modules'),
}, },
{ {
test: /\.node$/, test: /\.node$/,
loader: "node-loader", loader: 'node-loader',
}, },
]; ];
const plugins = [ const plugins = [
new CleanWebpackPlugin(), new CleanWebpackPlugin(),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [{ from: "./src/locales", to: "locales" }], patterns: [
{ from: './src/locales', to: 'locales' },
],
}), }),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
"process.env.BWCLI_ENV": JSON.stringify(ENV), 'process.env.BWCLI_ENV': JSON.stringify(ENV),
}), }),
new webpack.BannerPlugin({ new webpack.BannerPlugin({
banner: "#!/usr/bin/env node", banner: '#!/usr/bin/env node',
raw: true, raw: true
}),
new webpack.IgnorePlugin({
resourceRegExp: /^encoding$/,
contextRegExp: /node-fetch/,
}), }),
new webpack.IgnorePlugin(/^encoding$/, /node-fetch/),
]; ];
const config = { const config = {
mode: ENV, mode: ENV,
target: "node", target: 'node',
devtool: ENV === "development" ? "eval-source-map" : "source-map", devtool: ENV === 'development' ? 'eval-source-map' : 'source-map',
node: { node: {
__dirname: false, __dirname: false,
__filename: false, __filename: false,
}, },
entry: { entry: {
bwdc: "./src/bwdc.ts", 'bwdc': './src/bwdc.ts',
}, },
optimization: { optimization: {
minimize: false, minimize: false,
}, },
resolve: { resolve: {
extensions: [".ts", ".js", ".json"], extensions: ['.ts', '.js', '.json'],
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })], plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
symlinks: false, symlinks: false,
modules: [path.resolve("node_modules")], modules: [path.resolve('node_modules')],
}, },
output: { output: {
filename: "[name].js", filename: '[name].js',
path: path.resolve(__dirname, "build-cli"), path: path.resolve(__dirname, 'build-cli'),
}, },
module: { rules: moduleRules }, module: { rules: moduleRules },
plugins: plugins, plugins: plugins,

View File

@@ -1,45 +1,45 @@
const path = require("path"); const path = require('path');
const { merge } = require("webpack-merge"); const { merge } = require('webpack-merge');
const CopyWebpackPlugin = require("copy-webpack-plugin"); 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 nodeExternals = require('webpack-node-externals');
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const common = { const common = {
module: { module: {
rules: [ rules: [
{ {
test: /\.ts$/, test: /\.ts$/,
enforce: "pre", enforce: 'pre',
loader: "tslint-loader", loader: 'tslint-loader',
}, },
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: "ts-loader", use: 'ts-loader',
exclude: /node_modules\/(?!(@bitwarden)\/).*/, exclude: /node_modules\/(?!(@bitwarden)\/).*/,
}, },
], ],
}, },
plugins: [], plugins: [],
resolve: { resolve: {
extensions: [".tsx", ".ts", ".js"], extensions: ['.tsx', '.ts', '.js'],
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })], plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
}, },
output: { output: {
filename: "[name].js", filename: '[name].js',
path: path.resolve(__dirname, "build"), path: path.resolve(__dirname, 'build'),
}, },
}; };
const main = { const main = {
mode: "production", mode: 'production',
target: "electron-main", target: 'electron-main',
node: { node: {
__dirname: false, __dirname: false,
__filename: false, __filename: false,
}, },
entry: { entry: {
main: "./src/main.ts", 'main': './src/main.ts',
}, },
optimization: { optimization: {
minimize: false, minimize: false,
@@ -48,7 +48,7 @@ const main = {
rules: [ rules: [
{ {
test: /\.node$/, test: /\.node$/,
loader: "node-loader", loader: 'node-loader',
}, },
], ],
}, },
@@ -56,9 +56,9 @@ const main = {
new CleanWebpackPlugin(), new CleanWebpackPlugin(),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [ patterns: [
"./src/package.json", './src/package.json',
{ from: "./src/images", to: "images" }, { from: './src/images', to: 'images' },
{ from: "./src/locales", to: "locales" }, { from: './src/locales', to: 'locales' },
], ],
}), }),
], ],

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