1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-15 07:43:27 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Vincent Salucci
66fa3634ff [Version] Bump to 2.9.2 (#115)
* [Version] Bump to 2.10.0

* Downgraded to 2.9.2
2021-04-22 12:38:37 -05:00
121 changed files with 18964 additions and 27735 deletions

View File

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

View File

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

View File

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

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

@@ -1,237 +1,52 @@
---
name: Build
on:
push:
branches-ignore:
- 'l10n_master'
paths-ignore:
- '.github/workflows/**'
workflow_dispatch: {}
workflow_dispatch:
inputs:
jobs:
cloc:
name: CLOC
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
uses: actions/checkout@v2
- name: Set up CLOC
- name: Set up cloc
run: |
sudo apt update
sudo apt -y install cloc
- name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
setup:
name: Setup
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
outputs:
package_version: ${{ steps.retrieve-version.outputs.package_version }}
package_version: ${{ steps.get_version.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
uses: actions/checkout@v2
- name: Get Package Version
id: retrieve-version
id: get_version
shell: pwsh
run: |
PKG_VERSION=$(jq -r .version src/package.json)
echo "::set-output name=package_version::$PKG_VERSION"
$env:pkgVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).version
echo "::set-output name=PACKAGE_VERSION::$env:pkgVersion"
linux-cli:
name: Build Linux CLI
runs-on: ubuntu-20.04
cli:
runs-on: windows-latest
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_PKG_FETCH_NODE_VERSION: 16.13.0
_PKG_FETCH_VERSION: 3.2
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
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@6673cd052c4cd6fcf4b4e6e60ea986c889389535
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@6673cd052c4cd6fcf4b4e6e60ea986c889389535
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@a12a3943b4bdde767164f792f33f40b04645d846
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
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@6673cd052c4cd6fcf4b4e6e60ea986c889389535
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@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
windows-cli:
name: Build Windows CLI
runs-on: windows-2019
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_WIN_PKG_FETCH_VERSION: 16.13.0
_WIN_PKG_VERSION: 3.2
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
uses: actions/checkout@v2
- name: Setup Windows builder
run: |
@@ -239,88 +54,62 @@ jobs:
choco install reshack --no-progress
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
uses: actions/setup-node@v1
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: '16'
node-version: '10.x'
- name: Update NPM
- name: Setting WIN_PKG
run: |
npm install -g node-gyp
node-gyp install $(node -v)
echo "WIN_PKG=$env:WIN_PKG" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "version: $env:pkgVersion"
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v2.5\fetched-v10.4.1-win-x64
- name: Get pkg-fetch
- name: get pkg-fetch
shell: pwsh
run: |
cd $HOME
$fetchedUrl = "https://github.com/vercel/pkg-fetch/releases/download/v$env:_WIN_PKG_VERSION/node-v$env:_WIN_PKG_FETCH_VERSION-win-x64"
$fetchedUrl = "https://github.com/vercel/pkg-fetch/releases/download/v2.5/uploaded-v2.5-node-v10.4.1-win-x64"
New-Item -ItemType directory -Path ./.pkg-cache
New-Item -ItemType directory -Path ./.pkg-cache/v$env:_WIN_PKG_VERSION
Invoke-RestMethod -Uri $fetchedUrl `
-OutFile "./.pkg-cache/v$env:_WIN_PKG_VERSION/fetched-v$env:_WIN_PKG_FETCH_VERSION-win-x64"
New-Item -ItemType directory -Path ./.pkg-cache/v2.5
Invoke-RestMethod -Uri $fetchedUrl -OutFile "./.pkg-cache/v2.5/fetched-v10.4.1-win-x64"
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v2.5\fetched-v10.4.1-win-x64
- name: Keytar
shell: pwsh
run: |
$keytarVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).dependencies.keytar
$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"
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}"
New-Item -ItemType directory -Path ./keytar/macos | Out-Null
New-Item -ItemType directory -Path ./keytar/linux | Out-Null
New-Item -ItemType directory -Path ./keytar/windows | Out-Null
Invoke-RestMethod -Uri $($keytarUrl -f "darwin") -OutFile "./keytar/macos/$($keytarTarGz -f "darwin")"
Invoke-RestMethod -Uri $($keytarUrl -f "linux") -OutFile "./keytar/linux/$($keytarTarGz -f "linux")"
Invoke-RestMethod -Uri $($keytarUrl -f "win32") -OutFile "./keytar/windows/$($keytarTarGz -f "win32")"
7z e "./keytar/macos/$($keytarTarGz -f "darwin")" -o"./keytar/macos"
7z e "./keytar/linux/$($keytarTarGz -f "linux")" -o"./keytar/linux"
7z e "./keytar/windows/$($keytarTarGz -f "win32")" -o"./keytar/windows"
7z e "./keytar/macos/$($keytarTar -f "darwin")" -o"./keytar/macos"
7z e "./keytar/linux/$($keytarTar -f "linux")" -o"./keytar/linux"
7z e "./keytar/windows/$($keytarTar -f "win32")" -o"./keytar/windows"
- name: Setup Version Info
shell: pwsh
run: |
$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
run: ./scripts/make-versioninfo.ps1
- name: Resource Hacker
shell: cmd
run: |
set PATH=%PATH%;C:\Program Files (x86)\Resource Hacker
set WIN_PKG=C:\Users\runneradmin\.pkg-cache\v%_WIN_PKG_VERSION%\fetched-v%_WIN_PKG_FETCH_VERSION%-win-x64
set WIN_PKG_BUILT=C:\Users\runneradmin\.pkg-cache\v%_WIN_PKG_VERSION%\built-v%_WIN_PKG_FETCH_VERSION%-win-x64
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action delete -mask ICONGROUP,1,
ResourceHacker -open version-info.rc -save version-info.res -action compile
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action addoverwrite -resource version-info.res
@@ -329,68 +118,92 @@ jobs:
run: npm install
- name: Package CLI
run: npm run dist:cli:win
run: npm run dist:cli
- name: Zip
shell: cmd
run: 7z a ./dist-cli/bwdc-windows-%_PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node
run: |
7z a ./dist-cli/bwdc-windows-%PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node
7z a ./dist-cli/bwdc-macos-%PACKAGE_VERSION%.zip ./dist-cli/macos/bwdc ./keytar/macos/keytar.node
7z a ./dist-cli/bwdc-linux-%PACKAGE_VERSION%.zip ./dist-cli/linux/bwdc ./keytar/linux/keytar.node
- name: Version Test
run: |
Expand-Archive -Path "./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
Expand-Archive -Path "./dist-cli/bwdc-windows-${env:PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
$testVersion = Invoke-Expression '& ./test/windows/bwdc.exe -v'
echo "version: $env:_PACKAGE_VERSION"
echo "version: $env:PACKAGE_VERSION"
echo "testVersion: $testVersion"
if($testVersion -ne $env:_PACKAGE_VERSION) {
if($testVersion -ne $env:PACKAGE_VERSION) {
Throw "Version test failed."
}
- name: Create checksums
run: |
checksum -f="./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:_PACKAGE_VERSION}.txt
checksum -f="./dist-cli/bwdc-windows-${env:PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:PACKAGE_VERSION}.txt
checksum -f="./dist-cli/bwdc-macos-${env:PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-macos-sha256-${env:PACKAGE_VERSION}.txt
checksum -f="./dist-cli/bwdc-linux-${env:PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-linux-sha256-${env:PACKAGE_VERSION}.txt
- name: Upload Windows Zip to GitHub
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
- name: Upload windows zip to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
name: bwdc-windows-${{ env.PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env.PACKAGE_VERSION }}.zip
- name: Upload Windows checksum to GitHub
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
- name: Upload mac zip to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
name: bwdc-macos-${{ env.PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env.PACKAGE_VERSION }}.zip
- name: Upload linux zip to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-linux-${{ env.PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env.PACKAGE_VERSION }}.zip
- name: Upload windows checksum to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-windows-sha256-${{ env.PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-windows-sha256-${{ env.PACKAGE_VERSION }}.txt
- name: Upload mac checksum to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-macos-sha256-${{ env.PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-macos-sha256-${{ env.PACKAGE_VERSION }}.txt
- name: Upload linux checksum to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: bwdc-linux-sha256-${{ env.PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-linux-sha256-${{ env.PACKAGE_VERSION }}.txt
windows-gui:
name: Build Windows GUI
runs-on: windows-2019
windows_gui:
runs-on: windows-latest
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Set up .NET
uses: actions/setup-dotnet@9211491ffb35dd6a6657ca4f45d43dfe6e97c829
- name: Set up dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
uses: actions/setup-node@v1
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: '16'
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
@@ -403,13 +216,33 @@ jobs:
dotnet --version
- name: Install AST
uses: bitwarden/gh-actions/install-ast@f135c42c8596cb535c5bcb7523c0b2eef89709ac
shell: pwsh
run: |
cd $HOME
git clone https://github.com/vcsjones/AzureSignTool.git
cd AzureSignTool
$latest_head = $(git rev-parse HEAD)[0..9] -join ""
$latest_version = "0.0.0-g$latest_head"
Write-Host "--------"
Write-Host "git commit - $(git rev-parse HEAD)"
Write-Host "latest_head - $latest_head"
Write-Host "PACKAGE VERSION TO BUILD - $latest_version"
Write-Host "--------"
dotnet restore
dotnet pack --output ./nupkg
dotnet tool install --global --ignore-failed-sources --add-source ./nupkg --version $latest_version azuresigntool
- name: Checkout repo
uses: actions/checkout@v2
- name: Install Node dependencies
run: npm install
# - name: Run linter
# run: npm run lint
- name: Run linter
run: npm run lint
- name: Build & Sign
run: npm run dist:win
@@ -421,56 +254,34 @@ jobs:
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
- name: Upload Portable Executable to GitHub
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
- name: List Dist
run: dir ./dist
- name: Publish Portable Exe to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
name: Bitwarden-Connector-Portable-${{ env.PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env.PACKAGE_VERSION }}.exe
- name: Upload Installer Executable to GitHub
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
- name: Publish Installer Exe to GitHub
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable Blockmap to GitHub
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
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@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: latest.yml
path: ./dist/latest.yml
if-no-files-found: error
name: Bitwarden-Connector-Installer-${{ env.PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env.PACKAGE_VERSION }}.exe
linux-gui:
name: Build Linux GUI
runs-on: ubuntu-20.04
linux:
runs-on: ubuntu-latest
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
uses: actions/setup-node@v1
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: '16'
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
@@ -481,51 +292,36 @@ jobs:
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev
sudo apt-get -y install rpm
- name: NPM Install
- name: Checkout repo
uses: actions/checkout@v2
- name: npm install
run: npm install
- name: NPM Rebuild
- name: npm rebuild
run: npm run rebuild
- name: NPM Package
- name: npm package
run: npm run dist:lin
- name: Upload AppImage
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
- name: Publish AppImage
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
if-no-files-found: error
name: Bitwarden-Connector-${{ env.PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env.PACKAGE_VERSION }}-x86_64.AppImage
macos-gui:
name: Build MacOS GUI
runs-on: macos-11
macos:
runs-on: macos-latest
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
uses: actions/setup-node@v1
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: '16'
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
@@ -534,165 +330,61 @@ jobs:
run: |
node --version
npm --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
shell: bash
Write-Output "GitHub ref: $env:GITHUB_REF"
Write-Output "GitHub event: $env:GITHUB_EVENT"
shell: pwsh
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Checkout repo
uses: actions/checkout@v2
- name: Decrypt secrets
run: ./.github/scripts/macos/decrypt-secrets.ps1
shell: pwsh
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
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
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 }}
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
run: |
$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;
run: ./.github/scripts/load-version.ps1
shell: pwsh
- name: Install Node dependencies
run: npm install
# - name: Run linter
# run: npm run lint
- name: Run linter
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
env:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
- name: Rename Zip Artifact
run: |
cd dist
mv "Bitwarden Directory Connector-${{ env._PACKAGE_VERSION }}-mac.zip" \
"Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip"
- name: Upload .zip artifact
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
if-no-files-found: error
name: Bitwarden-Connector-${{ env.PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env.PACKAGE_VERSION }}-mac.zip
- name: Upload .dmg artifact
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc'
uses: actions/upload-artifact@v2
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
if-no-files-found: error
- name: Upload .dmg Blockmap artifact
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
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@6673cd052c4cd6fcf4b4e6e60ea986c889389535
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@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
if: failure()
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
if: failure()
with:
keyvault: "bitwarden-prod-kv"
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
with:
status: ${{ job.status }}
name: Bitwarden-Connector-${{ env.PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env.PACKAGE_VERSION }}.dmg

View File

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

View File

@@ -1,97 +1,392 @@
---
name: Release
on:
workflow_dispatch:
inputs:
release_type:
description: 'Release Options'
release_tag_name_input:
description: "Release Tag Name <X.X.X>"
required: true
default: 'Initial Release'
type: choice
options:
- Initial Release
- Redeploy
- Dry Run
jobs:
setup:
name: Setup
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
outputs:
package_version: ${{ steps.create_tags.outputs.package_version }}
tag_version: ${{ steps.create_tags.outputs.tag_version }}
release_upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Branch check
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
echo "==================================="
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
echo "==================================="
exit 1
fi
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
uses: actions/checkout@v2
- name: Retrieve Directory Connector release version
id: retrieve-version
- name: Create Release Vars
id: create_tags
run: |
PKG_VERSION=$(jq -r .version src/package.json)
echo "::set-output name=package_version::$PKG_VERSION"
case "${RELEASE_TAG_NAME_INPUT:0:1}" in
v)
echo "RELEASE_NAME=${RELEASE_TAG_NAME_INPUT:1}" >> $GITHUB_ENV
echo "RELEASE_TAG_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "::set-output name=package_version::${RELEASE_TAG_NAME_INPUT:1}"
echo "::set-output name=tag_version::$RELEASE_TAG_NAME_INPUT"
;;
[0-9])
echo "RELEASE_NAME=$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "RELEASE_TAG_NAME=v$RELEASE_TAG_NAME_INPUT" >> $GITHUB_ENV
echo "::set-output name=package_version::$RELEASE_TAG_NAME_INPUT"
echo "::set-output name=tag_version::v$RELEASE_TAG_NAME_INPUT"
;;
*)
exit 1
;;
esac
env:
RELEASE_TAG_NAME_INPUT: ${{ github.event.inputs.release_tag_name_input }}
- name: Check to make sure Mobile release version has been bumped
if: ${{ github.event.inputs.release_type == 'Initial Release' }}
- name: Create Draft Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
latest_ver=$(hub release -L 1 -f '%T')
latest_ver=${latest_ver:1}
echo "Latest version: $latest_ver"
ver=${{ steps.retrieve-version.outputs.package_version }}
echo "Version: $ver"
if [ "$latest_ver" = "$ver" ]; then
echo "Version has not been bumped!"
exit 1
fi
shell: bash
- name: Get branch name
id: branch
run: |
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch-name::$BRANCH_NAME"
- name: Download all artifacts
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ steps.branch.outputs.branch-name }}
- name: Create release
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
env:
PKG_VERSION: ${{ steps.retrieve-version.outputs.package_version }}
with:
artifacts: "./bwdc-windows-${{ env.PKG_VERSION }}.zip,
./bwdc-macos-${{ env.PKG_VERSION }}.zip,
./bwdc-linux-${{ env.PKG_VERSION }}.zip,
./bwdc-windows-sha256-${{ env.PKG_VERSION }}.txt,
./bwdc-macos-sha256-${{ env.PKG_VERSION }}.txt,
./bwdc-linux-sha256-${{ env.PKG_VERSION }}.txt,
./Bitwarden-Connector-Portable-${{ env.PKG_VERSION }}.exe,
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe,
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe.blockmap,
./Bitwarden-Connector-${{ env.PKG_VERSION }}-x86_64.AppImage,
./Bitwarden-Connector-${{ env.PKG_VERSION }}-mac.zip,
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg,
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg.blockmap,
./latest-linux.yml,
./latest-mac.yml,
./latest.yml"
commit: ${{ github.sha }}
tag: v${{ env.PKG_VERSION }}
name: Version ${{ env.PKG_VERSION }}
body: "<insert release notes here>"
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ env.RELEASE_TAG_NAME }}
release_name: ${{ env.RELEASE_NAME }}
draft: true
prerelease: false
cli:
runs-on: windows-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Setup Windows builder
run: |
choco install checksum --no-progress
choco install reshack --no-progress
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Set VER_INFO
run: |
echo "WIN_PKG=$env:WIN_PKG" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v2.5\fetched-v10.4.1-win-x64
- name: get pkg-fetch
shell: pwsh
run: |
cd $HOME
$fetchedUrl = "https://github.com/vercel/pkg-fetch/releases/download/v2.5/uploaded-v2.5-node-v10.4.1-win-x64"
New-Item -ItemType directory -Path ./.pkg-cache
New-Item -ItemType directory -Path ./.pkg-cache/v2.5
Invoke-RestMethod -Uri $fetchedUrl -OutFile "./.pkg-cache/v2.5/fetched-v10.4.1-win-x64"
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v2.5\fetched-v10.4.1-win-x64
- name: Keytar
shell: pwsh
run: |
$keytarVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).dependencies.keytar
$nodeModVersion = node -e "console.log(process.config.variables.node_module_version)"
$keytarTar = "keytar-v${keytarVersion}-node-v${nodeModVersion}-{0}-x64.tar"
$keytarTarGz = "${keytarTar}.gz"
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}"
New-Item -ItemType directory -Path ./keytar/macos | Out-Null
New-Item -ItemType directory -Path ./keytar/linux | Out-Null
New-Item -ItemType directory -Path ./keytar/windows | Out-Null
Invoke-RestMethod -Uri $($keytarUrl -f "darwin") -OutFile "./keytar/macos/$($keytarTarGz -f "darwin")"
Invoke-RestMethod -Uri $($keytarUrl -f "linux") -OutFile "./keytar/linux/$($keytarTarGz -f "linux")"
Invoke-RestMethod -Uri $($keytarUrl -f "win32") -OutFile "./keytar/windows/$($keytarTarGz -f "win32")"
7z e "./keytar/macos/$($keytarTarGz -f "darwin")" -o"./keytar/macos"
7z e "./keytar/linux/$($keytarTarGz -f "linux")" -o"./keytar/linux"
7z e "./keytar/windows/$($keytarTarGz -f "win32")" -o"./keytar/windows"
7z e "./keytar/macos/$($keytarTar -f "darwin")" -o"./keytar/macos"
7z e "./keytar/linux/$($keytarTar -f "linux")" -o"./keytar/linux"
7z e "./keytar/windows/$($keytarTar -f "win32")" -o"./keytar/windows"
- name: Setup Version Info
shell: pwsh
run: ./scripts/make-versioninfo.ps1
- name: Resource Hacker
shell: cmd
run: |
set PATH=%PATH%;C:\Program Files (x86)\Resource Hacker
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action delete -mask ICONGROUP,1,
ResourceHacker -open version-info.rc -save version-info.res -action compile
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action addoverwrite -resource version-info.res
- name: Install
run: npm install
- name: Package CLI
run: npm run dist:cli
- name: Zip
shell: cmd
run: |
7z a ./dist-cli/bwdc-windows-%PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node
7z a ./dist-cli/bwdc-macos-%PACKAGE_VERSION%.zip ./dist-cli/macos/bwdc ./keytar/macos/keytar.node
7z a ./dist-cli/bwdc-linux-%PACKAGE_VERSION%.zip ./dist-cli/linux/bwdc ./keytar/linux/keytar.node
- name: Version Test
run: |
Expand-Archive -Path "./dist-cli/bwdc-windows-${env:PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
$testVersion = Invoke-Expression '& ./test/windows/bwdc.exe -v'
echo "version: $env:PACKAGE_VERSION"
echo "testVersion: $testVersion"
if($testVersion -ne $env:PACKAGE_VERSION) {
Throw "Version test failed."
}
- name: Create checksums
run: |
checksum -f="./dist-cli/bwdc-windows-${env:PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:PACKAGE_VERSION}.txt
checksum -f="./dist-cli/bwdc-macos-${env:PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-macos-sha256-${env:PACKAGE_VERSION}.txt
checksum -f="./dist-cli/bwdc-linux-${env:PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-linux-sha256-${env:PACKAGE_VERSION}.txt
- name: upload windows zip release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-windows-${{ env.PACKAGE_VERSION }}.zip
asset_name: bwdc-windows-${{ env.PACKAGE_VERSION }}.zip
asset_content_type: application/zip
- name: upload macos zip release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-macos-${{ env.PACKAGE_VERSION }}.zip
asset_name: bwdc-macos-${{ env.PACKAGE_VERSION }}.zip
asset_content_type: application/zip
- name: upload linux zip release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-linux-${{ env.PACKAGE_VERSION }}.zip
asset_name: bwdc-linux-${{ env.PACKAGE_VERSION }}.zip
asset_content_type: application/zip
- name: upload windows checksum release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-windows-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_name: bwdc-windows-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_content_type: text/plain
- name: upload macos checksum release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-macos-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_name: bwdc-macos-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_content_type: text/plain
- name: upload linux checksum release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.setup.outputs.release_upload_url }}
asset_path: ./dist-cli/bwdc-linux-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_name: bwdc-linux-sha256-${{ env.PACKAGE_VERSION }}.txt
asset_content_type: text/plain
windows-gui:
runs-on: windows-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Set up dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
shell: pwsh
- name: Print environment
run: |
node --version
npm --version
dotnet --version
- name: Install AST
shell: pwsh
run: |
cd $HOME
git clone https://github.com/vcsjones/AzureSignTool.git
cd AzureSignTool
$latest_head = $(git rev-parse HEAD)[0..9] -join ""
$latest_version = "0.0.0-g$latest_head"
Write-Host "--------"
Write-Host "git commit - $(git rev-parse HEAD)"
Write-Host "latest_head - $latest_head"
Write-Host "PACKAGE VERSION TO BUILD - $latest_version"
Write-Host "--------"
dotnet restore
dotnet pack --output ./nupkg
dotnet tool install --global --ignore-failed-sources --add-source ./nupkg --version $latest_version azuresigntool
cd $HOME
- name: Checkout repo
uses: actions/checkout@v2
- name: Install Node dependencies
run: npm install
- name: Run linter
run: npm run lint
- name: npm rebuild
run: npm run rebuild
- name: Build & Sign
run: |
npm run publish:win
env:
ELECTRON_BUILDER_SIGN: 1
SIGNING_VAULT_URL: ${{ secrets.SIGNING_VAULT_URL }}
SIGNING_CLIENT_ID: ${{ secrets.SIGNING_CLIENT_ID }}
SIGNING_TENANT_ID: ${{ secrets.SIGNING_TENANT_ID }}
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
linux:
runs-on: ubuntu-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
- name: Set up environment
run: |
sudo apt-get update
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev
sudo apt-get -y install rpm
- name: Checkout repo
uses: actions/checkout@v2
- name: Set PACKAGE_VERSION
shell: pwsh
run: |
$env:pkgVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).version
echo "PACKAGE_VERSION=$env:pkgVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "version: $env:pkgVersion"
- name: npm install
run: npm install
- name: npm rebuild
run: npm run rebuild
- name: npm package
run: npm run publish:lin
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
macos:
runs-on: macos-latest
needs: setup
env:
PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
- name: Print environment
run: |
node --version
npm --version
Write-Output "GitHub ref: $env:GITHUB_REF"
Write-Output "GitHub event: $env:GITHUB_EVENT"
shell: pwsh
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }}
- name: Checkout repo
uses: actions/checkout@v2
- name: Decrypt secrets
run: ./.github/scripts/macos/decrypt-secrets.ps1
shell: pwsh
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
- name: Set up keychain
run: ./.github/scripts/macos/setup-keychain.ps1
shell: pwsh
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
DEVID_CERT_PASSWORD: ${{ secrets.DEVID_CERT_PASSWORD }}
MACDEV_CERT_PASSWORD: ${{ secrets.MACDEV_CERT_PASSWORD }}
- name: Load package version
run: ./.github/scripts/load-version.ps1
shell: pwsh
- name: Install Node dependencies
run: npm install
- name: Run linter
run: npm run lint
- name: Build application (dist)
run: npm run publish:mac
env:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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

View File

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

1
.husky/.gitignore vendored
View File

@@ -1 +0,0 @@
_

View File

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

View File

@@ -1,12 +0,0 @@
# Build directories
build
build-cli
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",
"protocol": "inspector",
"cwd": "${workspaceRoot}/build",
"runtimeArgs": ["--remote-debugging-port=9223", "."],
"runtimeArgs": [
"--remote-debugging-port=9223",
"."
],
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
@@ -28,13 +31,18 @@
"protocol": "inspector",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/build-cli/bwdc.js",
"args": ["sync"]
"args": [
"sync"
]
}
],
"compounds": [
{
"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.
Supported directories:
- Active Directory
- Any other LDAP-based directory
- Azure Active Directory
@@ -15,7 +14,7 @@ Supported directories:
The application is written using Electron with Angular and installs on Windows, macOS, and Linux distributions.
[![Platforms](https://imgur.com/SLv9paA.png "Windows, macOS, and Linux")](https://bitwarden.com/help/directory-sync/#download-and-install)
[![Platforms](https://imgur.com/SLv9paA.png "Windows, macOS, and Linux")](https://help.bitwarden.com/article/directory-sync/#download-and-install)
![Directory Connector](https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/directory-connector-macos.png "Dashboard")
@@ -42,13 +41,13 @@ bwdc config --help
**Detailed Documentation**
We provide detailed documentation and examples for using the Directory Connector CLI in our help center at https://bitwarden.com/help/directory-sync-cli/.
We provide detailed documentation and examples for using the Directory Connector CLI in our help center at https://help.bitwarden.com/article/directory-sync/#command-line-interface.
## Build/Run
**Requirements**
- [Node.js](https://nodejs.org) v16.13.1 (LTS)
- [Node.js](https://nodejs.org/)
- 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**
@@ -74,32 +73,8 @@ You can then run commands from the `./build-cli` folder:
node ./build-cli/bwdc.js --help
```
## We're Hiring!
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
### Prettier
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
1. Check out your local Branch
2. Run `git merge 225073aa335d33ad905877b68336a9288e89ea10`
3. Resolve any merge conflicts, commit.
4. Run `npm run prettier`
5. Commit
6. Run `git merge -Xours 096196fcd512944d1c3d9c007647a1319b032639`
7. Push
#### Git blame
We also recommend that you configure git to ignore the prettier revision using:
```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

View File

@@ -1,11 +1,39 @@
Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We welcome working with you to resolve the issue promptly. Thanks in advance!
Bitwarden believes that working with security researchers across the globe is crucial to keeping our
users safe. If you believe you've found a security issue in our product or service, we encourage you to
notify us. We welcome working with you to resolve the issue promptly. Thanks in advance!
# Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder.
- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder.
- If you would like to encrypt your report, please use the PGP key with long ID
`0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
# In-scope
- Security issues in any current release of Bitwarden. This includes the web vault, browser extension,
and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source
code is available at https://github.com/bitwarden.
# Exclusions
The following bug classes are out-of scope:
- Bugs that are already reported on any of Bitwarden's issue trackers (https://github.com/bitwarden),
or that we already know of. Note that some of our issue tracking is private.
- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the
upstream maintainer.
- Attacks requiring physical access to a user's device.
- Self-XSS
- Issues related to software or protocols not under Bitwarden's control
- Vulnerabilities in outdated versions of Bitwarden
- Missing security best practices that do not directly lead to a vulnerability
- Issues that do not have any impact on the general public
While researching, we'd like to ask you to refrain from:
@@ -14,8 +42,4 @@ While researching, we'd like to ask you to refrain from:
- Social engineering (including phishing) of Bitwarden staff or contractors
- Any physical attempts against Bitwarden property or data centers
# We want to help you!
If you have something that you feel is close to exploitation, or if you'd like some information regarding the internal API, or generally have any questions regarding the app that would help in your efforts, please email us at https://bitwarden.com/contact and ask for that information. As stated above, Bitwarden wants to help you find issues, and is more than willing to help.
Thank you for helping keep Bitwarden and our users safe!

31
gulpfile.js Normal file
View File

@@ -0,0 +1,31 @@
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));
}
// ref: https://github.com/angular/angular/issues/22524
function cleanupAotIssue() {
return del(['./node_modules/@types/uglify-js/node_modules/source-map/source-map.d.ts']);
}
exports.clean = clean;
exports.cleanupAotIssue = cleanupAotIssue;
exports.webfonts = gulp.series(clean, webfonts);
exports['prebuild:renderer'] = gulp.parallel(webfonts, cleanupAotIssue);;

2
jslib

Submodule jslib updated: 9950fb42a1...3c872e56f2

29624
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.",
"version": "0.0.0",
"keywords": [
@@ -20,18 +21,18 @@
"sub:update": "git submodule update --remote",
"sub:pull": "git submodule foreach git pull origin master",
"sub:commit": "npm run sub:pull && git commit -am \"update submodule\"",
"preinstall": "npm run sub:init",
"postinstall": "npm run sub:init",
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
"symlink:mac": "npm run symlink:lin",
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"rebuild": "electron-rebuild",
"rebuild": "./node_modules/.bin/electron-rebuild",
"reset": "rimraf ./node_modules/keytar/* && npm install",
"lint": "eslint . && prettier --check .",
"lint:fix": "eslint . --fix",
"lint": "tslint 'src/**/*.ts' || true",
"lint:fix": "tslint 'src/**/*.ts' --fix",
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
"build:main": "webpack --config webpack.main.js",
"build:renderer": "webpack --config webpack.renderer.js",
"build:renderer:watch": "webpack --config webpack.renderer.js --watch",
"build:renderer": "gulp prebuild:renderer && webpack --config webpack.renderer.js",
"build:renderer:watch": "gulp prebuild:renderer && webpack --config webpack.renderer.js --watch",
"build:dist": "npm run reset && npm run rebuild && npm run build",
"build:cli": "webpack --config webpack.cli.js",
"build:cli:watch": "webpack --config webpack.cli.js --watch",
@@ -58,17 +59,11 @@
"dist:cli:lin": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:lin",
"publish:lin": "npm run build:dist && npm run clean:dist && electron-builder --linux --x64 -p always",
"publish:mac": "npm run build:dist && npm run clean:dist && electron-builder --mac -p always",
"publish:win": "npm run build:dist && npm run clean:dist && electron-builder --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
"prettier": "prettier --write .",
"prepare": "husky install"
"publish:win": "npm run build:dist && npm run clean:dist && electron-builder --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\""
},
"build": {
"extraMetadata": {
"name": "bitwarden-directory-connector"
},
"productName": "Bitwarden Directory Connector",
"appId": "com.bitwarden.directory-connector",
"copyright": "Copyright © 2015-2022 Bitwarden Inc.",
"copyright": "Copyright © 2015-2020 Bitwarden Inc.",
"directories": {
"buildResources": "resources",
"output": "dist",
@@ -137,84 +132,95 @@
}
},
"devDependencies": {
"@angular/compiler-cli": "^12.2.13",
"@angular/compiler-cli": "^9.1.12",
"@microsoft/microsoft-graph-types": "^1.4.0",
"@ngtools/webpack": "^12.2.13",
"@types/ldapjs": "^1.0.10",
"@types/node": "^16.11.12",
"@ngtools/webpack": "^9.1.12",
"@types/commander": "^2.12.2",
"@types/form-data": "^2.2.1",
"@types/inquirer": "^0.0.43",
"@types/ldapjs": "^1.0.3",
"@types/lowdb": "^1.0.5",
"@types/lunr": "^2.3.3",
"@types/node": "^10.17.28",
"@types/node-fetch": "^2.5.10",
"@types/node-forge": "^0.9.7",
"@types/papaparse": "^5.2.0",
"@types/proper-lockfile": "^4.1.1",
"@typescript-eslint/eslint-plugin": "^5.12.1",
"@typescript-eslint/parser": "^5.12.1",
"clean-webpack-plugin": "^4.0.0",
"concurrently": "^6.0.2",
"copy-webpack-plugin": "^10.0.0",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"electron-builder": "^22.14.5",
"electron-notarize": "^1.1.1",
"electron-rebuild": "^3.2.5",
"@types/semver": "^5.5.0",
"@types/source-map": "0.5.2",
"@types/webcrypto": "^0.0.28",
"@types/webpack": "^4.4.11",
"@types/zxcvbn": "4.4.0",
"clean-webpack-plugin": "^0.1.19",
"concurrently": "^4.0.1",
"copy-webpack-plugin": "^5.1.1",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"del": "^3.0.0",
"electron": "11.3.0",
"electron-builder": "22.9.1",
"electron-notarize": "^1.0.0",
"electron-rebuild": "^2.3.5",
"electron-reload": "^1.5.0",
"eslint": "^8.9.0",
"eslint-config-prettier": "^8.4.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.4",
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"husky": "^7.0.4",
"lint-staged": "^12.1.3",
"mini-css-extract-plugin": "^2.4.5",
"node-loader": "^2.0.0",
"pkg": "^5.5.1",
"prebuild-install": "^5.0.0",
"prettier": "^2.5.1",
"rimraf": "^3.0.2",
"sass": "^1.32.11",
"sass-loader": "^12.4.0",
"tapable": "^1.1.3",
"ts-loader": "^9.2.5",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "4.3.5",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1",
"webpack-merge": "^5.8.0",
"webpack-node-externals": "^3.0.0"
"file-loader": "^2.0.0",
"font-awesome": "4.7.0",
"gulp": "^4.0.0",
"gulp-google-webfonts": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.9.0",
"node-abi": "^2.9.0",
"node-loader": "^0.6.0",
"node-sass": "^4.13.1",
"pkg": "4.3.8",
"rimraf": "^2.6.2",
"sass-loader": "^7.1.0",
"ts-loader": "^7.0.5",
"tslint": "^5.12.1",
"tslint-loader": "^3.5.4",
"typescript": "3.8.3",
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1",
"webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"@angular/animations": "^12.2.13",
"@angular/cdk": "^12.2.13",
"@angular/common": "^12.2.13",
"@angular/compiler": "^12.2.13",
"@angular/core": "^12.2.13",
"@angular/forms": "^12.2.13",
"@angular/platform-browser": "^12.2.13",
"@angular/platform-browser-dynamic": "^12.2.13",
"@angular/router": "^12.2.13",
"@bitwarden/jslib-angular": "file:jslib/angular",
"@bitwarden/jslib-common": "file:jslib/common",
"@bitwarden/jslib-electron": "file:jslib/electron",
"@bitwarden/jslib-node": "file:jslib/node",
"@microsoft/microsoft-graph-client": "^2.2.1",
"bootstrap": "^4.6.0",
"chalk": "^4.1.1",
"commander": "^7.2.0",
"core-js": "^3.11.0",
"@angular/animations": "9.1.12",
"@angular/common": "9.1.12",
"@angular/compiler": "9.1.12",
"@angular/core": "9.1.12",
"@angular/forms": "9.1.12",
"@angular/platform-browser": "9.1.12",
"@angular/platform-browser-dynamic": "9.1.12",
"@angular/router": "9.1.12",
"@angular/upgrade": "9.1.12",
"@microsoft/microsoft-graph-client": "1.2.0",
"angular2-toaster": "8.0.0",
"big-integer": "1.6.48",
"bootstrap": "4.3.1",
"browser-hrtime": "^1.1.8",
"chalk": "2.4.1",
"commander": "7.0.0",
"core-js": "2.6.2",
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
"form-data": "^4.0.0",
"googleapis": "^73.0.0",
"inquirer": "8.0.0",
"ldapjs": "2.3.1",
"lunr": "^2.3.9",
"ngx-toastr": "14.1.4",
"open": "^8.0.6",
"electron-log": "4.3.0",
"electron-store": "6.0.1",
"electron-updater": "4.3.5",
"form-data": "2.3.2",
"googleapis": "43.0.0",
"https-proxy-agent": "5.0.0",
"inquirer": "6.2.0",
"keytar": "7.3.0",
"ldapjs": "git+https://git@github.com/kspearrin/node-ldapjs.git",
"lowdb": "1.0.0",
"lunr": "2.3.3",
"node-fetch": "2.6.1",
"node-forge": "0.10.0",
"open": "7.1.0",
"proper-lockfile": "^4.1.2",
"rxjs": "^7.4.0"
},
"engines": {
"node": "~16",
"npm": "~8"
},
"lint-staged": {
"./!(jslib)**": "prettier --ignore-unknown --write",
"*.ts": "eslint --fix"
"rxjs": "6.6.2",
"tslib": "^2.0.1",
"zone.js": "0.10.3",
"zxcvbn": "4.4.2"
}
}

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

View File

@@ -1,7 +1,9 @@
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
exports.default = async function (configuration) {
if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") {
console.log(`[*] Signing file: ${configuration.path}`);
exports.default = async function(configuration) {
if (
parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 &&
configuration.path.slice(-4) == ".exe"
) {
console.log(`[*] Signing file: ${configuration.path}`)
require("child_process").execSync(
`azuresigntool sign ` +
`-kvu ${process.env.SIGNING_VAULT_URL} ` +
@@ -14,7 +16,7 @@ exports.default = async function (configuration) {
`-tr http://timestamp.digicert.com ` +
`"${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,8 +1,8 @@
{
"name": "@bitwarden/directory-connector",
"name": "bitwarden-directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2.9.5",
"version": "2.9.0",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
@@ -19,6 +19,6 @@
},
"dependencies": {
"browser-hrtime": "^1.1.8",
"keytar": "^7.7.0"
"keytar": "7.3.0"
}
}

View File

@@ -1,66 +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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
import {
Component,
ComponentFactoryResolver,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { Router } from '@angular/router';
import { EnvironmentComponent } from './environment.component';
import { AuthService } from 'jslib/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service';
import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/login.component';
import { ModalComponent } from 'jslib/angular/components/modal.component';
@Component({
selector: 'app-login',
templateUrl: 'login.component.html',
})
export class LoginComponent extends BaseLoginComponent {
@ViewChild('environment', { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef;
constructor(authService: AuthService, router: Router,
i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
super(authService, router,
platformUtilsService, i18nService,
stateService, environmentService,
passwordGenerationService, cryptoFunctionService,
storageService);
super.successRoute = '/tabs/dashboard';
}
settings() {
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
const modal = this.environmentModal.createComponent(factory).instance;
const childComponent = modal.show<EnvironmentComponent>(EnvironmentComponent,
this.environmentModal);
childComponent.onSaved.subscribe(() => {
modal.close();
});
}
sso() {
return super.launchSsoBrowser('connector', 'bwdc://sso-callback');
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,70 @@
<div class="container-fluid">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="card">
<h5 class="card-header">{{title}}</h5>
<div class="card-body">
<ng-container
*ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
<p *ngIf="selectedProviderType === providerType.Authenticator">
{{'enterVerificationCodeApp' | i18n}}
</p>
<p *ngIf="selectedProviderType === providerType.Email">
{{'enterVerificationCodeEmail' | i18n : twoFactorEmail}}
</p>
<div class="form-group">
<label for="code">{{'verificationCode' | i18n}}</label>
<input id="code" type="text" name="Code" [(ngModel)]="token" appAutofocus
class="form-control">
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
<p>{{'insertYubiKey' | i18n}}</p>
<p><img src="../../images/yubikey.jpg" class="img-fluid rounded" alt=""></p>
<div class="form-group">
<label for="code">{{'verificationCode' | i18n}}</label>
<input id="code" type="password" name="Code" [(ngModel)]="token" appAutofocus
class="form-control">
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo">
<div id="duo-frame">
<iframe id="duo_iframe"></iframe>
</div>
</ng-container>
<div class="form-group"
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.U2f">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="remember" [(ngModel)]="remember"
name="Remember">
<label class="form-check-label" for="remember">{{'rememberMe' | i18n}}</label>
</div>
</div>
<ng-container class="card-body"
*ngIf="selectedProviderType === null || selectedProviderType === providerType.U2f">
<p>{{'noTwoStepProviders' | i18n}}</p>
<p>{{'noTwoStepProviders2' | i18n}}</p>
</ng-container>
<button type="submit" class="btn btn-primary" [disabled]="form.loading" *ngIf="selectedProviderType != null && selectedProviderType !== providerType.U2f && selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo">
<i class="fa fa-sign-in fa-fw" [hidden]="form.loading"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!form.loading"></i>
{{'continue' | i18n}}
</button>
<a routerLink="/login" class="btn btn-link">{{'cancel' | i18n}}</a>
</div>
</div>
<div class="text-center mt-3">
<a href="#" appStopClick (click)="anotherMethod()">{{'useAnotherTwoStepMethod' | i18n}}</a>
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise"
*ngIf="selectedProviderType === providerType.Email">
{{'sendVerificationCodeEmailAgain' | i18n}}
</a>
</div>
</div>
</div>
</form>
</div>
<ng-template #twoFactorOptions></ng-template>

View File

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

View File

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

View File

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

View File

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

16
src/app/dummy.module.ts Normal file
View File

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

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
import { InjectionToken } from "@angular/core";
export const USE_SECURE_STORAGE_FOR_SECRETS = new InjectionToken<boolean>(
"USE_SECURE_STORAGE_FOR_SECRETS"
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,30 @@
<div class="row">
<div class="col-sm">
<div class="card">
<h3 class="card-header">{{ "about" | i18n }}</h3>
<h3 class="card-header">{{'about' | i18n}}</h3>
<div class="card-body">
<p>
{{ "bitwardenDirectoryConnector" | i18n }}
<br />
{{ "version" | i18n: version }} <br />
&copy; Bitwarden Inc. LLC 2015-{{ year }}
{{'bitwardenDirectoryConnector' | i18n}}
<br /> {{'version' | i18n : version}}
<br /> &copy; Bitwarden Inc. LLC 2015-{{year}}
</p>
<button
class="btn btn-primary"
type="button"
(click)="update()"
[disabled]="checkingForUpdate"
>
<i class="bwi bwi-download bwi-fw" [hidden]="checkingForUpdate"></i>
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!checkingForUpdate"></i>
{{ "checkForUpdates" | i18n }}
<button class="btn btn-primary" type="button" (click)="update()" [disabled]="checkingForUpdate">
<i class="fa fa-download fa-fw" [hidden]="checkingForUpdate"></i>
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!checkingForUpdate"></i>
{{'checkForUpdates' | i18n}}
</button>
</div>
</div>
</div>
<div class="col-sm">
<div class="card">
<h3 class="card-header">{{ "other" | i18n }}</h3>
<h3 class="card-header">{{'other' | i18n}}</h3>
<div class="card-body">
<button class="btn btn-primary" type="button" (click)="logOut()">
{{ "logOut" | i18n }}
{{'logOut' | i18n}}
</button>
<button class="btn btn-primary" type="button" (click)="clearCache()">
{{ "clearSyncCache" | i18n }}
{{'clearSyncCache' | i18n}}
</button>
</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 { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ToasterService } from 'angular2-toaster';
import { StateService } from "../../abstractions/state.service";
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
const BroadcasterSubscriptionId = "MoreComponent";
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { ConfigurationService } from '../../services/configuration.service';
const BroadcasterSubscriptionId = 'MoreComponent';
@Component({
selector: "app-more",
templateUrl: "more.component.html",
selector: 'app-more',
templateUrl: 'more.component.html',
})
export class MoreComponent implements OnInit {
version: string;
year: string;
checkingForUpdate = false;
constructor(
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private messagingService: MessagingService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private changeDetectorRef: ChangeDetectorRef,
private stateService: StateService
) {}
constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private messagingService: MessagingService, private configurationService: ConfigurationService,
private toasterService: ToasterService, private broadcasterService: BroadcasterService,
private ngZone: NgZone, private changeDetectorRef: ChangeDetectorRef) { }
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "checkingForUpdate":
case 'checkingForUpdate':
this.checkingForUpdate = true;
break;
case "doneCheckingForUpdate":
case 'doneCheckingForUpdate':
this.checkingForUpdate = false;
break;
default:
@@ -55,23 +59,20 @@ export class MoreComponent implements OnInit {
}
update() {
this.messagingService.send("checkForUpdate");
this.messagingService.send('checkForUpdate');
}
async logOut() {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("logOutConfirmation"),
this.i18nService.t("logOut"),
this.i18nService.t("yes"),
this.i18nService.t("cancel")
);
this.i18nService.t('logOutConfirmation'), this.i18nService.t('logOut'),
this.i18nService.t('yes'), this.i18nService.t('cancel'));
if (confirmed) {
this.messagingService.send("logout");
this.messagingService.send('logout');
}
}
async clearCache() {
await this.stateService.clearSyncSettings(true);
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncCacheCleared"));
await this.configurationService.clearStatefulSettings(true);
this.toasterService.popAsync('success', null, this.i18nService.t('syncCacheCleared'));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
src/global.d.ts vendored
View File

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

View File

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

View File

@@ -20,30 +20,12 @@
"emailRequired": {
"message": "Email address is required."
},
"clientIdRequired": {
"message": "Client Id is required."
},
"invalidClientId": {
"message": "Invalid Client Id provided."
},
"clientSecretRequired": {
"message": "Client Secret is required."
},
"orgApiKeyRequired": {
"message": "Api Key must belong to an Organization"
},
"failedToSaveCredentials": {
"message": "Failed to save credentials"
},
"invalidEmail": {
"message": "Invalid email address."
},
"masterPassRequired": {
"message": "Master password is required."
},
"missingRequiredInput": {
"message": "Missing required input."
},
"unexpectedError": {
"message": "An unexpected error has occurred."
},
@@ -354,9 +336,6 @@
"rootPath": {
"message": "Root Path"
},
"identityAuthority": {
"message": "Identity Authority"
},
"tenant": {
"message": "Tenant"
},
@@ -423,12 +402,6 @@
"groupFilter": {
"message": "Group Filter"
},
"syncGroupsOneLogin": {
"message": "Sync roles"
},
"groupFilterOneLogin": {
"message": "Role Filter"
},
"groupObjectClass": {
"message": "Group Object Class"
},
@@ -444,19 +417,6 @@
"sync": {
"message": "Sync"
},
"duplicateEmails": {
"message": "Emails must be unique. Multiple entries pulled with the following emails:",
"desription": "Error message displayed when duplicate email addresses are synced. Followed by a list of duplicate emails."
},
"andMore": {
"message": "and $NUMBER$ more...",
"placeholders": {
"NUMBER": {
"content": "$1",
"example": "10"
}
}
},
"ldapEncrypted": {
"message": "This server uses an encrypted connection"
},
@@ -609,7 +569,7 @@
"message": "Welcome to the Bitwarden Directory Connector"
},
"logInDesc": {
"message": "Log in with an organization API key below."
"message": "Log in as an organization admin user below."
},
"dirConfigIncomplete": {
"message": "Directory configuration incomplete."
@@ -643,9 +603,6 @@
}
}
},
"largeImport": {
"message": "More than 2000 users or groups are expected to sync."
},
"overwriteExisting": {
"message": "Overwrite existing organization users based on current sync settings."
},

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
// eslint-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 implements IConfiguration {
identityAuthority: string;
export class AzureConfiguration {
tenant: string;
applicationId: string;
key: string;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1784
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",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2.10.0",
"version": "2.9.2",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
@@ -13,10 +13,9 @@
},
"dependencies": {
"browser-hrtime": "^1.1.8",
"electron-log": "4.4.1",
"electron-store": "8.0.1",
"electron-updater": "4.6.1",
"keytar": "7.7.0",
"rxjs": "^7.4.0"
"electron-log": "4.3.0",
"electron-store": "6.0.1",
"electron-updater": "4.3.5",
"keytar": "7.3.0"
}
}

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ h1 {
small {
color: $text-muted;
font-size: $h1-font-size * 0.5;
font-size: $h1-font-size * .5;
}
}
@@ -28,7 +28,7 @@ h4 {
}
#duo-frame {
background: url("../images/loading.svg") 0 0 no-repeat;
background: url('../images/loading.svg') 0 0 no-repeat;
height: 380px;
iframe {
@@ -118,26 +118,3 @@ ul.testing-list {
}
}
.btn[class*="btn-outline-"] {
&:not(:hover) {
border-color: $secondary;
background-color: #fbfbfb;
}
}
.btn-outline-secondary {
color: $text-muted;
&:hover:not(:disabled) {
color: $body-color;
}
&:disabled {
opacity: 1;
}
&:focus,
&.focus {
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), 0.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";
.toast-container {
#toast-container {
.toast-close-button {
font-size: 18px;
margin-right: 4px;
right: -0.15em;
}
.ngx-toastr {
align-items: center;
.toast {
opacity: 1 !important;
background-image: none !important;
border-radius: $border-radius;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
display: flex;
padding: 15px;
.toast-close-button {
position: absolute;
right: 5px;
top: 0;
}
align-items: center;
&:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
}
.icon i::before {
float: left;
font-style: normal;
font-family: $icomoon-font-family;
&:before {
font-family: FontAwesome;
font-size: 25px;
line-height: 20px;
padding-right: 15px;
float: left;
color: #ffffff;
padding-right: 10px;
margin: auto 0 auto -36px;
}
.toaster-icon {
display: none;
}
.toast-message {
@@ -45,36 +45,40 @@
}
}
&.toast-danger,
&.toast-error {
&.toast-danger, &.toast-error {
background-image: none !important;
background-color: $danger;
.icon i::before {
content: map_get($icons, "error");
&:before {
content: "\f0e7";
margin-left: -30px;
}
}
&.toast-warning {
background-image: none !important;
background-color: $warning;
.icon i::before {
content: map_get($icons, "exclamation-triangle");
&:before {
content: "\f071";
}
}
&.toast-info {
background-image: none !important;
background-color: $info;
.icon i:before {
content: map_get($icons, "info-circle");
&:before {
content: "\f05a";
}
}
&.toast-success {
background-image: none !important;
background-color: $success;
.icon i:before {
content: map_get($icons, "check");
&:before {
content: "\f00C";
}
}
}

View File

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

View File

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

View File

@@ -1,68 +0,0 @@
import { Injectable } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AppIdService } from "jslib-common/abstractions/appId.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
import { AuthResult } from "jslib-common/models/domain/authResult";
import { ApiLogInCredentials } from "jslib-common/models/domain/logInCredentials";
import { AuthService as AuthServiceBase } from "jslib-common/services/auth.service";
import { StateService } from "../abstractions/state.service";
import { OrganizationLogInStrategy } from "../misc/logInStrategies/organizationLogIn.strategy";
@Injectable()
export class AuthService extends AuthServiceBase {
constructor(
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
keyConnectorService: KeyConnectorService,
environmentService: EnvironmentService,
stateService: StateService,
twoFactorService: TwoFactorService,
i18nService: I18nService
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
keyConnectorService,
environmentService,
stateService,
twoFactorService,
i18nService
);
}
async logIn(credentials: ApiLogInCredentials): Promise<AuthResult> {
const strategy = new OrganizationLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService
);
return strategy.logIn(credentials);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,229 @@
import { DirectoryType } from '../enums/directoryType';
import { StorageService } from 'jslib/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 { UserEntry } from "../models/userEntry";
import { GroupEntry } from '../models/groupEntry';
import { UserEntry } from '../models/userEntry';
export interface IDirectoryService {
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;

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