mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-05 23:53:21 +00:00
Compare commits
489 Commits
v2.0.0
...
refactor/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acc3473731 | ||
|
|
c80805ded6 | ||
|
|
c75d26b618 | ||
|
|
13a13dd18f | ||
|
|
954b23d91f | ||
|
|
b9d35c3dc7 | ||
|
|
536f48b3c7 | ||
|
|
8cd768c7c2 | ||
|
|
b233d2e87d | ||
|
|
1f6d8c1458 | ||
|
|
ae05183aa3 | ||
|
|
8374103a15 | ||
|
|
dd9e03843a | ||
|
|
e38ce53ed5 | ||
|
|
0c21bcf847 | ||
|
|
1c6b94e640 | ||
|
|
ef1c47ab19 | ||
|
|
64ff16e895 | ||
|
|
89860d6770 | ||
|
|
91ff43a17f | ||
|
|
0f19ebc928 | ||
|
|
b48a1d5856 | ||
|
|
7776009a31 | ||
|
|
ff816035ce | ||
|
|
fe384b14f0 | ||
|
|
adeb84f44e | ||
|
|
dc2e17c5db | ||
|
|
f3d8b39ac5 | ||
|
|
3be1f2eac6 | ||
|
|
1146c8f5bf | ||
|
|
910bfb945d | ||
|
|
4e886c1c15 | ||
|
|
a4b85f1e30 | ||
|
|
7c85c9fddd | ||
|
|
68c964acaa | ||
|
|
1be64836f4 | ||
|
|
f2389189a3 | ||
|
|
bb4be6022b | ||
|
|
f85a0c5ea5 | ||
|
|
5afae04b1d | ||
|
|
d1b182d20b | ||
|
|
9e3d1caee4 | ||
|
|
9a78956b23 | ||
|
|
822655b944 | ||
|
|
6dfbe505d9 | ||
|
|
0809c2c104 | ||
|
|
e30000bd00 | ||
|
|
90a7601960 | ||
|
|
8a800c6d33 | ||
|
|
d0021c9306 | ||
|
|
97673c84da | ||
|
|
771a182235 | ||
|
|
857d725a77 | ||
|
|
25b3e0f691 | ||
|
|
d2ba7631b5 | ||
|
|
a893c78c74 | ||
|
|
5ff041aa7b | ||
|
|
096196fcd5 | ||
|
|
225073aa33 | ||
|
|
f8b26d82d8 | ||
|
|
6b98a46b94 | ||
|
|
13572b94ee | ||
|
|
999b790557 | ||
|
|
7c93d59a42 | ||
|
|
9bec2aa2f0 | ||
|
|
240e1d5813 | ||
|
|
d82f4d90c1 | ||
|
|
abc68e8ef9 | ||
|
|
660ee538ce | ||
|
|
a96144d6dc | ||
|
|
e43d192007 | ||
|
|
74a018edb8 | ||
|
|
07d0049183 | ||
|
|
5f5358ea0f | ||
|
|
36cc6552bf | ||
|
|
05b5fd2eb4 | ||
|
|
95f1e86509 | ||
|
|
378dd06274 | ||
|
|
314adeb164 | ||
|
|
cc4f8c9f8d | ||
|
|
35b0e81beb | ||
|
|
9136e3936b | ||
|
|
35aead6c0e | ||
|
|
615f3b82db | ||
|
|
baa441cb90 | ||
|
|
9ad683ca09 | ||
|
|
c2d1d12cd2 | ||
|
|
3b6bac7668 | ||
|
|
2be879548d | ||
|
|
033c346042 | ||
|
|
32a8e65fe8 | ||
|
|
b2d4d80181 | ||
|
|
56c1cb23a0 | ||
|
|
ba26f70d1a | ||
|
|
e5589e7664 | ||
|
|
4e82486784 | ||
|
|
bb1cdebaf4 | ||
|
|
01405f47c9 | ||
|
|
5e64dc9262 | ||
|
|
9c7cd943b3 | ||
|
|
7cf3166169 | ||
|
|
9bdb77a573 | ||
|
|
3b8ee5ec0d | ||
|
|
6e7e09064f | ||
|
|
dfcb450a8a | ||
|
|
b192c34c15 | ||
|
|
f813dbb690 | ||
|
|
16deafca76 | ||
|
|
647b087fa7 | ||
|
|
4bd1387b83 | ||
|
|
4e098462dc | ||
|
|
0b1c2ae72a | ||
|
|
5d3fa0a0d2 | ||
|
|
6097bca063 | ||
|
|
a6aafe7593 | ||
|
|
56d05af07a | ||
|
|
0d17345600 | ||
|
|
5df62b7422 | ||
|
|
868914feb1 | ||
|
|
1a9555d4af | ||
|
|
33c8f15e45 | ||
|
|
ed8dd01dbd | ||
|
|
2296e37e8f | ||
|
|
a0f33c7bdc | ||
|
|
f6b249836e | ||
|
|
0c92a97054 | ||
|
|
24ab152559 | ||
|
|
5f9f09d77c | ||
|
|
4d27d9e48d | ||
|
|
0b624b972a | ||
|
|
1889e12bac | ||
|
|
7648f73072 | ||
|
|
75d346ed85 | ||
|
|
dabfe7907d | ||
|
|
965976223f | ||
|
|
410f00c213 | ||
|
|
0d8b942ad4 | ||
|
|
5371015a58 | ||
|
|
2ead70e434 | ||
|
|
ffca14cb5f | ||
|
|
090d5e82df | ||
|
|
8893ddf0f7 | ||
|
|
997ec5a699 | ||
|
|
762818ee39 | ||
|
|
61c6ba8189 | ||
|
|
9cfa646bcb | ||
|
|
b4301c7d41 | ||
|
|
71b5f6a38a | ||
|
|
1c0052fe30 | ||
|
|
35862acb73 | ||
|
|
11cf64fcc7 | ||
|
|
2ab37b45cf | ||
|
|
7096fc830b | ||
|
|
39806b7d96 | ||
|
|
7a16b8cb0e | ||
|
|
2583068dbd | ||
|
|
e5d0b3a372 | ||
|
|
9a1caf1e7e | ||
|
|
af0e41e26c | ||
|
|
d3049164a9 | ||
|
|
bdfda6775d | ||
|
|
72e6e74a42 | ||
|
|
75832fbed9 | ||
|
|
811b3adb51 | ||
|
|
e710df3ba7 | ||
|
|
4ee0c3ccba | ||
|
|
036b934119 | ||
|
|
4bfd43bf4c | ||
|
|
55722d3c04 | ||
|
|
77043d8d66 | ||
|
|
002117a6e5 | ||
|
|
5aa8097cfd | ||
|
|
19e1049566 | ||
|
|
87bdc88e22 | ||
|
|
950e3ae91e | ||
|
|
a37532e1ad | ||
|
|
21ff5f311b | ||
|
|
7dd12cf0cb | ||
|
|
6f8df7a690 | ||
|
|
1ff0ef1f90 | ||
|
|
196fc10d80 | ||
|
|
7496de4cc6 | ||
|
|
02a6adf6a2 | ||
|
|
6c95575a8f | ||
|
|
39755e89a8 | ||
|
|
c5d3ca218e | ||
|
|
87a2a2a0e4 | ||
|
|
5904da2eb1 | ||
|
|
1ac0c81661 | ||
|
|
955711714d | ||
|
|
5848553a4b | ||
|
|
38758caac4 | ||
|
|
b41a1bdbf4 | ||
|
|
a400ab7f7d | ||
|
|
e5d0405882 | ||
|
|
f2137c02f7 | ||
|
|
4c61f465a3 | ||
|
|
6d22041eab | ||
|
|
752e26db6d | ||
|
|
094ec23f04 | ||
|
|
af8ff2901e | ||
|
|
628689c990 | ||
|
|
ab37221182 | ||
|
|
626892473f | ||
|
|
c621677852 | ||
|
|
6650b4848d | ||
|
|
c07c56f89b | ||
|
|
294590882f | ||
|
|
9151b9c2d6 | ||
|
|
5817468d09 | ||
|
|
31dd20999c | ||
|
|
4eb9c9bd4d | ||
|
|
150164534f | ||
|
|
2b2d8a9fab | ||
|
|
fb122cbbdb | ||
|
|
0b37857d29 | ||
|
|
15c1876687 | ||
|
|
473a6e391d | ||
|
|
13ad64e6f3 | ||
|
|
04e278249e | ||
|
|
e12c4ea1e2 | ||
|
|
acfa8632af | ||
|
|
f53c1f5605 | ||
|
|
f0f7f89ea8 | ||
|
|
059ff0647a | ||
|
|
d94e5b0620 | ||
|
|
3840bce6d7 | ||
|
|
71cd11eedf | ||
|
|
ecea04bc08 | ||
|
|
0575ee5507 | ||
|
|
8b2fa8405b | ||
|
|
7d36a50687 | ||
|
|
f7dd9d8d5b | ||
|
|
379b4f4612 | ||
|
|
4652668e1b | ||
|
|
4e02a8571e | ||
|
|
bc927a65ac | ||
|
|
90f1a1c115 | ||
|
|
c48acf6038 | ||
|
|
2640e8c890 | ||
|
|
094ec55e7f | ||
|
|
fe39bdac42 | ||
|
|
2c98d50c43 | ||
|
|
d374cff51c | ||
|
|
d9e7256804 | ||
|
|
0ef2d1523e | ||
|
|
05bad6f671 | ||
|
|
5a62cfcda1 | ||
|
|
634d38510d | ||
|
|
bf27872973 | ||
|
|
20bb5a4926 | ||
|
|
f63fb3ffa0 | ||
|
|
f11c32c606 | ||
|
|
0b4e22a952 | ||
|
|
a85dbff3a5 | ||
|
|
d2835dd577 | ||
|
|
62abc99b61 | ||
|
|
20de62cc79 | ||
|
|
731614279f | ||
|
|
2be751dc8e | ||
|
|
e0409941a3 | ||
|
|
e6a5a3c8c1 | ||
|
|
3f3590a223 | ||
|
|
c1f64d7b82 | ||
|
|
f90611c96f | ||
|
|
630e21f7c1 | ||
|
|
2e81642c0e | ||
|
|
39514b9550 | ||
|
|
2da82d5610 | ||
|
|
69f33a08b6 | ||
|
|
a05e9c7746 | ||
|
|
d8031e4f49 | ||
|
|
e6aa07ba5c | ||
|
|
173129014a | ||
|
|
8d4baa6d31 | ||
|
|
20463ce653 | ||
|
|
2617f96710 | ||
|
|
53b0614faf | ||
|
|
a01db9c448 | ||
|
|
84dc8e3696 | ||
|
|
e7b988c042 | ||
|
|
a06212af1a | ||
|
|
b217ac9456 | ||
|
|
ff8422d6c1 | ||
|
|
b51d279ba6 | ||
|
|
78e4601413 | ||
|
|
5900db10eb | ||
|
|
c9bd853032 | ||
|
|
976fcad098 | ||
|
|
5d8193c348 | ||
|
|
cc09b26818 | ||
|
|
b0247caa4c | ||
|
|
38316abeae | ||
|
|
0991dfa6bd | ||
|
|
88029663e9 | ||
|
|
3f5cb10db6 | ||
|
|
1c927722f0 | ||
|
|
c04354ba1c | ||
|
|
3335659c8d | ||
|
|
f0282b33f0 | ||
|
|
f69d461463 | ||
|
|
bcf16562fe | ||
|
|
d7412410f2 | ||
|
|
d5907477df | ||
|
|
9acfaff361 | ||
|
|
737274da59 | ||
|
|
7a0e0e6915 | ||
|
|
25847b9c83 | ||
|
|
07dd7bb93e | ||
|
|
bc9936bd92 | ||
|
|
e4428e3387 | ||
|
|
30ef0ce140 | ||
|
|
38c461ebe0 | ||
|
|
ead1776f90 | ||
|
|
0e944b2442 | ||
|
|
3bbd503308 | ||
|
|
a7b13b168e | ||
|
|
1cf8ae1bdf | ||
|
|
7da3c8e252 | ||
|
|
2edf3fb68d | ||
|
|
1e698ba81b | ||
|
|
e9034ea6fe | ||
|
|
08ebc1ce16 | ||
|
|
a80c95e1cf | ||
|
|
c3ab785d66 | ||
|
|
baef6210c1 | ||
|
|
74d4a6fa72 | ||
|
|
770e41a657 | ||
|
|
d04eb03b3c | ||
|
|
9a8224a8d6 | ||
|
|
bd072b727c | ||
|
|
93cedec32c | ||
|
|
3d8933e5f0 | ||
|
|
790f5a5f2f | ||
|
|
36bb24d580 | ||
|
|
3e08ba95cf | ||
|
|
e50f82175b | ||
|
|
5907c8b5ce | ||
|
|
9f9b5beff9 | ||
|
|
71dbd31477 | ||
|
|
9e4d17cc7c | ||
|
|
2ae14913cc | ||
|
|
a4e23eee83 | ||
|
|
0bc406b332 | ||
|
|
76a6b2407e | ||
|
|
f371067d56 | ||
|
|
d1507dffca | ||
|
|
c11cc0b979 | ||
|
|
7cc941cc84 | ||
|
|
a847339d72 | ||
|
|
46827cdaa0 | ||
|
|
183b54f9b8 | ||
|
|
e51a70f7b9 | ||
|
|
f83e97abfc | ||
|
|
92a566e73e | ||
|
|
59ade0d2ba | ||
|
|
7da368d555 | ||
|
|
e00fd648a6 | ||
|
|
f74c8420fb | ||
|
|
d2fbb8aba9 | ||
|
|
ba8aee2148 | ||
|
|
018dfd8865 | ||
|
|
be40ee3100 | ||
|
|
35f9b5ed17 | ||
|
|
8c4450246b | ||
|
|
537e7d10e6 | ||
|
|
124c7288b9 | ||
|
|
3224648bc7 | ||
|
|
9b8a11b65e | ||
|
|
6b9d72f4a2 | ||
|
|
8d50697345 | ||
|
|
f8e9d2b73a | ||
|
|
99ed78a2e6 | ||
|
|
346dab2c26 | ||
|
|
28582b2b0c | ||
|
|
2bda7e5f65 | ||
|
|
8849f6c513 | ||
|
|
6d50856c07 | ||
|
|
bdec0f9493 | ||
|
|
ab31beb1ae | ||
|
|
014d995284 | ||
|
|
69783d3606 | ||
|
|
55a0078332 | ||
|
|
346e0194d4 | ||
|
|
512f222819 | ||
|
|
006c6b3312 | ||
|
|
8c80a52ce2 | ||
|
|
5d8bf54f2f | ||
|
|
70c1eb0623 | ||
|
|
447b674469 | ||
|
|
7cb2147569 | ||
|
|
327de4d714 | ||
|
|
9b57955d72 | ||
|
|
e63198e130 | ||
|
|
03eac9006f | ||
|
|
d3402cca5e | ||
|
|
076b320e70 | ||
|
|
44180f95a5 | ||
|
|
e23ebf13b7 | ||
|
|
e0fd1a2e93 | ||
|
|
c39986b37c | ||
|
|
1e0029dc15 | ||
|
|
63bc5e4161 | ||
|
|
8c8b4da595 | ||
|
|
1a7536270a | ||
|
|
d92e3c8d7b | ||
|
|
c30aedf491 | ||
|
|
0e96a462ee | ||
|
|
3ef60cb3a0 | ||
|
|
34b7638e11 | ||
|
|
63ef932469 | ||
|
|
e09163fc72 | ||
|
|
6a737f6d7d | ||
|
|
ccc7b7b213 | ||
|
|
1e98bc6430 | ||
|
|
c63a945057 | ||
|
|
a13d9fa00f | ||
|
|
1518d5eafc | ||
|
|
c74800fd8c | ||
|
|
9e282efe28 | ||
|
|
26efc9158d | ||
|
|
e2d3d35d71 | ||
|
|
c115bf84f0 | ||
|
|
5b56d32492 | ||
|
|
2fe294ad8a | ||
|
|
1343897fa9 | ||
|
|
9b2b0e4ea6 | ||
|
|
489e775b08 | ||
|
|
0d76a45181 | ||
|
|
4787f9a462 | ||
|
|
b228e12c81 | ||
|
|
0e28100ec6 | ||
|
|
d2c1697144 | ||
|
|
b924ea8dd1 | ||
|
|
30936d3ad1 | ||
|
|
289589dce9 | ||
|
|
48734b8109 | ||
|
|
2e69a41943 | ||
|
|
f414a3ac0a | ||
|
|
81833b134c | ||
|
|
5a5b74a0d5 | ||
|
|
5a7a230193 | ||
|
|
bc5ab6252e | ||
|
|
0e4d5e0973 | ||
|
|
08e254e34c | ||
|
|
2c20796ea1 | ||
|
|
8b9d7a7e2e | ||
|
|
1a0709b02c | ||
|
|
0f386ee8d2 | ||
|
|
c0b41155e9 | ||
|
|
ddd1165728 | ||
|
|
651dbe59c8 | ||
|
|
aeb6f28f9a | ||
|
|
012eefad4e | ||
|
|
b05346d4a3 | ||
|
|
854e6e84fe | ||
|
|
20436bf07d | ||
|
|
4110e4b8cb | ||
|
|
e179edc2e0 | ||
|
|
133ef7eb16 | ||
|
|
5a98982fc2 | ||
|
|
f6e92bf597 | ||
|
|
af4a8d5ae8 | ||
|
|
c3704bbb67 | ||
|
|
8620f2f80d | ||
|
|
e0124474cf | ||
|
|
ac21b78225 | ||
|
|
dca69a5428 | ||
|
|
6b1fa0f50c | ||
|
|
3ab35dc4e0 | ||
|
|
2aa1fc1acb | ||
|
|
e6643fa4cb | ||
|
|
4848dd8502 | ||
|
|
4b08d7b4b2 | ||
|
|
839189c772 | ||
|
|
a06ff45c21 | ||
|
|
c598efcb9f | ||
|
|
878476d195 | ||
|
|
82e599fc13 | ||
|
|
79b76687a0 | ||
|
|
af5349c4cb | ||
|
|
3de35b481e | ||
|
|
008bb308e7 | ||
|
|
9b44fc4a35 | ||
|
|
3d374fc792 | ||
|
|
b51c842d18 | ||
|
|
95b595d5bf | ||
|
|
445c3f220a | ||
|
|
9644d95b9b |
@@ -7,10 +7,9 @@ root = true
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
quote_type = single
|
||||
|
||||
# Set default charset
|
||||
[*.{js,ts,scss,html}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
indent_size = 2
|
||||
|
||||
9
.eslintignore
Normal file
9
.eslintignore
Normal file
@@ -0,0 +1,9 @@
|
||||
dist
|
||||
build
|
||||
build-cli
|
||||
jslib
|
||||
webpack.cli.js
|
||||
webpack.main.js
|
||||
webpack.renderer.js
|
||||
|
||||
**/node_modules
|
||||
32
.eslintrc.json
Normal file
32
.eslintrc.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": ["./jslib/shared/eslintrc.json"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"alphabetize": {
|
||||
"order": "asc"
|
||||
},
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "jslib-*/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "src/**/*",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
}
|
||||
],
|
||||
"pathGroupsExcludedImportTypes": ["builtin"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
||||
# Apply Prettier https://github.com/bitwarden/directory-connector/pull/194
|
||||
096196fcd512944d1c3d9c007647a1319b032639
|
||||
33
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
33
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
## Type of change
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature development
|
||||
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||
- [ ] Build/deploy pipeline (DevOps)
|
||||
- [ ] Other
|
||||
|
||||
## Objective
|
||||
|
||||
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
|
||||
|
||||
## Code changes
|
||||
|
||||
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
|
||||
<!--Also refer to any related changes or PRs in other repositories-->
|
||||
|
||||
- **file.ext:** Description of what was changed and why
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!--Required for any UI changes. Delete if not applicable-->
|
||||
|
||||
## Testing requirements
|
||||
|
||||
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
|
||||
|
||||
## Before you submit
|
||||
|
||||
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
|
||||
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
|
||||
- [ ] This change requires a **documentation update** (notify the documentation team)
|
||||
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
|
||||
BIN
.github/secrets/devid-app-cert.p12.gpg
vendored
Normal file
BIN
.github/secrets/devid-app-cert.p12.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/devid-installer-cert.p12.gpg
vendored
Normal file
BIN
.github/secrets/devid-installer-cert.p12.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/macdev-cert.p12.gpg
vendored
Normal file
BIN
.github/secrets/macdev-cert.p12.gpg
vendored
Normal file
Binary file not shown.
698
.github/workflows/build.yml
vendored
Normal file
698
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,698 @@
|
||||
---
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
paths-ignore:
|
||||
- '.github/workflows/**'
|
||||
workflow_dispatch: {}
|
||||
|
||||
|
||||
jobs:
|
||||
cloc:
|
||||
name: CLOC
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
|
||||
- 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
|
||||
outputs:
|
||||
package_version: ${{ steps.retrieve-version.outputs.package_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
|
||||
- name: Get Package Version
|
||||
id: retrieve-version
|
||||
run: |
|
||||
PKG_VERSION=$(jq -r .version src/package.json)
|
||||
echo "::set-output name=package_version::$PKG_VERSION"
|
||||
|
||||
|
||||
linux-cli:
|
||||
name: Build Linux CLI
|
||||
runs-on: ubuntu-20.04
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_PKG_FETCH_NODE_VERSION: 16.13.0
|
||||
_PKG_FETCH_VERSION: 3.2
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@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
|
||||
|
||||
- name: Setup Windows builder
|
||||
run: |
|
||||
choco install checksum --no-progress
|
||||
choco install reshack --no-progress
|
||||
|
||||
- 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
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
- 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"
|
||||
$keytarTarGz = "${keytarTar}.gz"
|
||||
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}"
|
||||
|
||||
New-Item -ItemType directory -Path ./keytar/windows | Out-Null
|
||||
|
||||
Invoke-RestMethod -Uri $($keytarUrl -f "win32") -OutFile "./keytar/windows/$($keytarTarGz -f "win32")"
|
||||
|
||||
7z e "./keytar/windows/$($keytarTarGz -f "win32")" -o"./keytar/windows"
|
||||
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
- name: Install
|
||||
run: npm install
|
||||
|
||||
- name: Package CLI
|
||||
run: npm run dist:cli:win
|
||||
|
||||
- name: Zip
|
||||
shell: cmd
|
||||
run: 7z a ./dist-cli/bwdc-windows-%_PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node
|
||||
|
||||
- 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
|
||||
|
||||
- name: Upload Windows Zip to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
with:
|
||||
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Windows checksum to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
with:
|
||||
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
windows-gui:
|
||||
name: Build Windows GUI
|
||||
runs-on: windows-2019
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@9211491ffb35dd6a6657ca4f45d43dfe6e97c829
|
||||
with:
|
||||
dotnet-version: "3.1.x"
|
||||
|
||||
- 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: Set Node options
|
||||
run: echo "NODE_OPTIONS=--max_old_space_size=4096" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
shell: pwsh
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
dotnet --version
|
||||
|
||||
- name: Install AST
|
||||
uses: bitwarden/gh-actions/install-ast@f135c42c8596cb535c5bcb7523c0b2eef89709ac
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
|
||||
# - name: Run linter
|
||||
# run: npm run lint
|
||||
|
||||
- name: Build & Sign
|
||||
run: npm run dist:win
|
||||
env:
|
||||
ELECTRON_BUILDER_SIGN: 1
|
||||
SIGNING_VAULT_URL: ${{ secrets.SIGNING_VAULT_URL }}
|
||||
SIGNING_CLIENT_ID: ${{ secrets.SIGNING_CLIENT_ID }}
|
||||
SIGNING_TENANT_ID: ${{ secrets.SIGNING_TENANT_ID }}
|
||||
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
|
||||
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
|
||||
|
||||
- name: Upload Portable Executable to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
with:
|
||||
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Installer Executable to GitHub
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
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
|
||||
|
||||
|
||||
linux-gui:
|
||||
name: Build Linux GUI
|
||||
runs-on: ubuntu-20.04
|
||||
needs: setup
|
||||
env:
|
||||
_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: 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: NPM Install
|
||||
run: npm install
|
||||
|
||||
- name: NPM Rebuild
|
||||
run: npm run rebuild
|
||||
|
||||
- name: NPM Package
|
||||
run: npm run dist:lin
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
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
|
||||
|
||||
|
||||
macos-gui:
|
||||
name: Build MacOS GUI
|
||||
runs-on: macos-11
|
||||
needs: setup
|
||||
env:
|
||||
_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: Set Node options
|
||||
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
shell: bash
|
||||
|
||||
- name: Decrypt secrets
|
||||
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
|
||||
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;
|
||||
shell: pwsh
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
|
||||
# - name: Run linter
|
||||
# run: npm run lint
|
||||
|
||||
- name: Build application
|
||||
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
|
||||
with:
|
||||
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
||||
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .dmg artifact
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
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 }}
|
||||
16
.github/workflows/enforce-labels.yml
vendored
Normal file
16
.github/workflows/enforce-labels.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Enforce PR labels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled, opened, edited, synchronize]
|
||||
jobs:
|
||||
enforce-label:
|
||||
name: EnforceLabel
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Enforce Label
|
||||
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
|
||||
with:
|
||||
BANNED_LABELS: "hold"
|
||||
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"
|
||||
97
.github/workflows/release.yml
vendored
Normal file
97
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_type:
|
||||
description: 'Release Options'
|
||||
required: true
|
||||
default: 'Initial Release'
|
||||
type: choice
|
||||
options:
|
||||
- Initial Release
|
||||
- Redeploy
|
||||
- Dry Run
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-20.04
|
||||
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
|
||||
|
||||
- name: Retrieve Directory Connector release version
|
||||
id: retrieve-version
|
||||
run: |
|
||||
PKG_VERSION=$(jq -r .version src/package.json)
|
||||
echo "::set-output name=package_version::$PKG_VERSION"
|
||||
|
||||
- name: Check to make sure Mobile release version has been bumped
|
||||
if: ${{ github.event.inputs.release_type == 'Initial Release' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
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 }}
|
||||
draft: true
|
||||
65
.github/workflows/version-bump.yml
vendored
Normal file
65
.github/workflows/version-bump.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: Version Bump
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_number:
|
||||
description: "New Version"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
|
||||
- name: Create Version Branch
|
||||
run: |
|
||||
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||
git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Checkout Version Branch
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
with:
|
||||
ref: version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Bump Version - Package
|
||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./src/package.json"
|
||||
|
||||
- name: Commit files
|
||||
run: |
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
||||
|
||||
- name: Push changes
|
||||
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Create Version PR
|
||||
env:
|
||||
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
BASE_BRANCH: master
|
||||
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
|
||||
run: |
|
||||
gh pr create --title "$TITLE" \
|
||||
--base "$BASE" \
|
||||
--head "$PR_BRANCH" \
|
||||
--label "version update" \
|
||||
--label "automated pr" \
|
||||
--body "
|
||||
## Type of change
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature development
|
||||
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||
- [ ] Build/deploy pipeline (DevOps)
|
||||
- [X] Other
|
||||
|
||||
## Objective
|
||||
Automated version bump to ${{ github.event.inputs.version_number }}"
|
||||
11
.github/workflows/workflow-linter.yml
vendored
Normal file
11
.github/workflows/workflow-linter.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Workflow Linter
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/**
|
||||
|
||||
jobs:
|
||||
call-workflow:
|
||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@master
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,10 +4,14 @@ node_modules
|
||||
npm-debug.log
|
||||
vwd.webinfo
|
||||
dist/
|
||||
dist-cli/
|
||||
css/
|
||||
*.crx
|
||||
*.pem
|
||||
build-cli/
|
||||
build/
|
||||
yarn-error.log
|
||||
.DS_Store
|
||||
*.nupkg
|
||||
*.provisionprofile
|
||||
*.env
|
||||
|
||||
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
12
.prettierignore
Normal file
12
.prettierignore
Normal file
@@ -0,0 +1,12 @@
|
||||
# Build directories
|
||||
build
|
||||
build-cli
|
||||
dist
|
||||
|
||||
jslib
|
||||
|
||||
# External libraries / auto synced locales
|
||||
src/locales
|
||||
|
||||
# Github Workflows
|
||||
.github/workflows
|
||||
3
.prettierrc.json
Normal file
3
.prettierrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"printWidth": 100
|
||||
}
|
||||
61
.vscode/launch.json
vendored
61
.vscode/launch.json
vendored
@@ -1,37 +1,40 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Electron: Main",
|
||||
"protocol": "inspector",
|
||||
"cwd": "${workspaceRoot}/build",
|
||||
"runtimeArgs": [
|
||||
"--remote-debugging-port=9223",
|
||||
"."
|
||||
],
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
||||
},
|
||||
"sourceMaps": true
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Electron: Main",
|
||||
"protocol": "inspector",
|
||||
"cwd": "${workspaceRoot}/build",
|
||||
"runtimeArgs": ["--remote-debugging-port=9223", "."],
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
||||
},
|
||||
{
|
||||
"name": "Electron: Renderer",
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"port": 9223,
|
||||
"webRoot": "${workspaceFolder}/build",
|
||||
"sourceMaps": true
|
||||
}
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"name": "Electron: Renderer",
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"port": 9223,
|
||||
"webRoot": "${workspaceFolder}/build",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug CLI",
|
||||
"protocol": "inspector",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"program": "${workspaceFolder}/build-cli/bwdc.js",
|
||||
"args": ["sync"]
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Electron: All",
|
||||
"configurations": [
|
||||
"Electron: Main",
|
||||
"Electron: Renderer"
|
||||
]
|
||||
}
|
||||
{
|
||||
"name": "Electron: All",
|
||||
"configurations": ["Electron: Main", "Electron: Renderer"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
78
README.md
78
README.md
@@ -1,4 +1,4 @@
|
||||
[](https://ci.appveyor.com/project/bitwarden/directory-connector)
|
||||

|
||||
[](https://gitter.im/bitwarden/Lobby)
|
||||
|
||||
# Bitwarden Directory Connector
|
||||
@@ -6,6 +6,7 @@
|
||||
The Bitwarden Directory Connector is a a desktop application used to sync your Bitwarden enterprise organization to an existing directory of users and groups.
|
||||
|
||||
Supported directories:
|
||||
|
||||
- Active Directory
|
||||
- Any other LDAP-based directory
|
||||
- Azure Active Directory
|
||||
@@ -14,24 +15,91 @@ Supported directories:
|
||||
|
||||
The application is written using Electron with Angular and installs on Windows, macOS, and Linux distributions.
|
||||
|
||||
<a href="https://bitwarden.com/#download"><img src="https://imgur.com/SLv9paA.png" width="500" height="113"></a>
|
||||
[](https://bitwarden.com/help/directory-sync/#download-and-install)
|
||||
|
||||
# Build/Run
|
||||

|
||||
|
||||
## Command-line Interface
|
||||
|
||||
A command-line interface tool is also available for the Bitwarden Directory Connector. The Directory Connector CLI (`bwdc`) is written with TypeScript and Node.js and can also be run on Windows, macOS, and Linux distributions.
|
||||
|
||||
## CLI Documentation
|
||||
|
||||
The Bitwarden Directory Connector CLI is self-documented with `--help` content and examples for every command. You should start exploring the CLI by using the global `--help` option:
|
||||
|
||||
```bash
|
||||
bwdc --help
|
||||
```
|
||||
|
||||
This option will list all available commands that you can use with the Directory Connector CLI.
|
||||
|
||||
Additionally, you can run the `--help` option on a specific command to learn more about it:
|
||||
|
||||
```
|
||||
bwdc test --help
|
||||
bwdc config --help
|
||||
```
|
||||
|
||||
**Detailed Documentation**
|
||||
|
||||
We provide detailed documentation and examples for using the Directory Connector CLI in our help center at https://bitwarden.com/help/directory-sync-cli/.
|
||||
|
||||
## Build/Run
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [Node.js](https://nodejs.org) v16.13.1 (LTS)
|
||||
- Windows users: To compile the native node modules used in the app you will need the Visual C++ toolset, available through the standard Visual Studio installer (recommended) or by installing [`windows-build-tools`](https://github.com/felixrieseberg/windows-build-tools) through `npm`. See more at [Compiling native Addon modules](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules).
|
||||
|
||||
**Run the app**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run reset # Only necessary if you have previously run the CLI app
|
||||
npm run rebuild
|
||||
npm run electron
|
||||
```
|
||||
|
||||
# Contribute
|
||||
**Run the CLI**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run reset # Only necessary if you have previously run the desktop app
|
||||
npm run build:cli:watch
|
||||
```
|
||||
|
||||
You can then run commands from the `./build-cli` folder:
|
||||
|
||||
```bash
|
||||
node ./build-cli/bwdc.js --help
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
42
SECURITY.md
42
SECURITY.md
@@ -1,39 +1,11 @@
|
||||
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!
|
||||
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!
|
||||
|
||||
# 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).
|
||||
|
||||
# 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
|
||||
- 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).
|
||||
|
||||
While researching, we'd like to ask you to refrain from:
|
||||
|
||||
@@ -42,4 +14,8 @@ 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!
|
||||
|
||||
2
jslib
2
jslib
Submodule jslib updated: 9de9c1655c...9950fb42a1
28214
package-lock.json
generated
28214
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
203
package.json
203
package.json
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": "bitwarden-directory-connector",
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"name": "@bitwarden/directory-connector",
|
||||
"description": "Sync your user directory to your Bitwarden organization.",
|
||||
"version": "0.0.0",
|
||||
"keywords": [
|
||||
@@ -9,7 +8,7 @@
|
||||
"vault",
|
||||
"password manager"
|
||||
],
|
||||
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -21,37 +20,67 @@
|
||||
"sub:update": "git submodule update --remote",
|
||||
"sub:pull": "git submodule foreach git pull origin master",
|
||||
"sub:commit": "npm run sub:pull && git commit -am \"update submodule\"",
|
||||
"postinstall": "./node_modules/.bin/electron-rebuild && npm run sub:init",
|
||||
"lint": "tslint src/**/*.ts || true",
|
||||
"lint:fix": "tslint src/**/*.ts --fix",
|
||||
"preinstall": "npm run sub:init",
|
||||
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
|
||||
"symlink:mac": "npm run symlink:lin",
|
||||
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
|
||||
"rebuild": "electron-rebuild",
|
||||
"reset": "rimraf ./node_modules/keytar/* && npm install",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
|
||||
"build:main": "webpack --config webpack.main.js",
|
||||
"build:renderer": "webpack --config webpack.renderer.js",
|
||||
"build:renderer:watch": "webpack --config webpack.renderer.js --watch",
|
||||
"build:dist": "npm run reset && npm run rebuild && npm run build",
|
||||
"build:cli": "webpack --config webpack.cli.js",
|
||||
"build:cli:watch": "webpack --config webpack.cli.js --watch",
|
||||
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.js",
|
||||
"build:cli:prod:watch": "cross-env NODE_ENV=production webpack --config webpack.cli.js --watch",
|
||||
"electron": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 ./build --watch\" \"npm run build:renderer:watch\"",
|
||||
"clean:dist": "rimraf ./dist/*",
|
||||
"pack:lin": "npm run clean:dist && build --linux --x64 -p never",
|
||||
"pack:mac": "npm run clean:dist && build --mac -p never",
|
||||
"pack:win": "npm run clean:dist && build --win --x64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
|
||||
"pack:win:ci": "npm run clean:dist && build --win --x64 --ia32 -p never",
|
||||
"dist:lin": "npm run build && npm run pack:lin",
|
||||
"dist:mac": "npm run build && npm run pack:mac",
|
||||
"dist:win": "npm run build && npm run pack:win",
|
||||
"clean:dist:cli": "rimraf ./dist-cli/*",
|
||||
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never",
|
||||
"pack:mac": "npm run clean:dist && electron-builder --mac -p never",
|
||||
"pack:win": "npm run clean:dist && electron-builder --win --x64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
|
||||
"pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --ia32 -p never",
|
||||
"pack:cli": "npm run pack:cli:win | npm run pack:cli:mac | npm run pack:cli:lin",
|
||||
"pack:cli:win": "pkg ./src-cli --targets win-x64 --output ./dist-cli/windows/bwdc.exe",
|
||||
"pack:cli:mac": "pkg ./src-cli --targets macos-x64 --output ./dist-cli/macos/bwdc",
|
||||
"pack:cli:lin": "pkg ./src-cli --targets linux-x64 --output ./dist-cli/linux/bwdc",
|
||||
"dist:lin": "npm run build:dist && npm run pack:lin",
|
||||
"dist:mac": "npm run build:dist && npm run pack:mac",
|
||||
"dist:win": "npm run build:dist && npm run pack:win",
|
||||
"dist:win:ci": "npm run build && npm run pack:win:ci",
|
||||
"publish:lin": "npm run build && npm run clean:dist && build --linux --x64 -p always",
|
||||
"publish:mac": "npm run build && npm run clean:dist && build --mac -p always",
|
||||
"publish:win": "npm run build && npm run clean:dist && build --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\""
|
||||
"dist:cli": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli",
|
||||
"dist:cli:win": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:win",
|
||||
"dist:cli:mac": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:mac",
|
||||
"dist:cli:lin": "npm run build:cli:prod && npm run clean:dist:cli && npm run pack:cli:lin",
|
||||
"publish:lin": "npm run build:dist && npm run clean:dist && electron-builder --linux --x64 -p always",
|
||||
"publish:mac": "npm run build:dist && npm run clean:dist && electron-builder --mac -p always",
|
||||
"publish:win": "npm run build:dist && npm run clean:dist && electron-builder --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
|
||||
"prettier": "prettier --write .",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"build": {
|
||||
"extraMetadata": {
|
||||
"name": "bitwarden-directory-connector"
|
||||
},
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"appId": "com.bitwarden.directory-connector",
|
||||
"copyright": "Copyright © 2015-2018 8bit Solutions LLC",
|
||||
"copyright": "Copyright © 2015-2022 Bitwarden Inc.",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "dist",
|
||||
"app": "build"
|
||||
},
|
||||
"afterSign": "scripts/notarize.js",
|
||||
"mac": {
|
||||
"category": "public.app-category.productivity",
|
||||
"gatekeeperAssess": false,
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "resources/entitlements.mac.plist",
|
||||
"entitlementsInherit": "resources/entitlements.mac.plist",
|
||||
"target": [
|
||||
"dmg",
|
||||
"zip"
|
||||
@@ -61,7 +90,8 @@
|
||||
"target": [
|
||||
"portable",
|
||||
"nsis"
|
||||
]
|
||||
],
|
||||
"sign": "scripts/sign.js"
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utility",
|
||||
@@ -107,67 +137,84 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "5.2.0",
|
||||
"@microsoft/microsoft-graph-types": "^1.2.0",
|
||||
"@ngtools/webpack": "1.10.2",
|
||||
"@types/keytar": "^4.0.1",
|
||||
"@types/ldapjs": "^1.0.3",
|
||||
"@types/lunr": "2.1.5",
|
||||
"@types/node": "8.0.19",
|
||||
"@types/node-forge": "0.7.1",
|
||||
"@types/webcrypto": "0.0.28",
|
||||
"clean-webpack-plugin": "^0.1.17",
|
||||
"concurrently": "3.5.1",
|
||||
"copy-webpack-plugin": "^4.2.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"electron": "1.8.4",
|
||||
"electron-builder": "^20.8.1",
|
||||
"electron-rebuild": "1.7.3",
|
||||
"electron-reload": "1.2.2",
|
||||
"extract-text-webpack-plugin": "^3.0.1",
|
||||
"file-loader": "^1.1.5",
|
||||
"font-awesome": "4.7.0",
|
||||
"google-fonts-webpack-plugin": "^0.4.4",
|
||||
"html-loader": "^0.5.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"node-loader": "^0.6.0",
|
||||
"node-sass": "^4.7.2",
|
||||
"rimraf": "^2.6.2",
|
||||
"sass-loader": "^6.0.6",
|
||||
"ts-loader": "^3.5.0",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-loader": "^3.5.3",
|
||||
"typescript": "^2.7.1",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-merge": "^4.1.0",
|
||||
"webpack-node-externals": "^1.6.0"
|
||||
"@angular/compiler-cli": "^12.2.13",
|
||||
"@microsoft/microsoft-graph-types": "^1.4.0",
|
||||
"@ngtools/webpack": "^12.2.13",
|
||||
"@types/ldapjs": "^1.0.10",
|
||||
"@types/node": "^16.11.12",
|
||||
"@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",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "5.2.0",
|
||||
"@angular/common": "5.2.0",
|
||||
"@angular/compiler": "5.2.0",
|
||||
"@angular/core": "5.2.0",
|
||||
"@angular/forms": "5.2.0",
|
||||
"@angular/http": "5.2.0",
|
||||
"@angular/platform-browser": "5.2.0",
|
||||
"@angular/platform-browser-dynamic": "5.2.0",
|
||||
"@angular/router": "5.2.0",
|
||||
"@angular/upgrade": "5.2.0",
|
||||
"@microsoft/microsoft-graph-client": "1.0.0",
|
||||
"@okta/okta-sdk-nodejs": "1.1.0",
|
||||
"angular2-toaster": "4.0.2",
|
||||
"angulartics2": "5.0.1",
|
||||
"bootstrap": "4.1.0",
|
||||
"core-js": "2.4.1",
|
||||
"electron-log": "2.2.14",
|
||||
"electron-store": "1.3.0",
|
||||
"electron-updater": "2.21.4",
|
||||
"googleapis": "29.0.0",
|
||||
"keytar": "4.1.0",
|
||||
"ldapjs": "1.0.2",
|
||||
"lunr": "2.1.6",
|
||||
"node-forge": "0.7.1",
|
||||
"rxjs": "5.5.6",
|
||||
"zone.js": "0.8.19"
|
||||
"@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",
|
||||
"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",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"rxjs": "^7.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "~16",
|
||||
"npm": "~8"
|
||||
},
|
||||
"lint-staged": {
|
||||
"./!(jslib)**": "prettier --ignore-unknown --write",
|
||||
"*.ts": "eslint --fix"
|
||||
}
|
||||
}
|
||||
|
||||
10
resources/entitlements.mac.plist
Normal file
10
resources/entitlements.mac.plist
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
19
scripts/notarize.js
Normal file
19
scripts/notarize.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
require("dotenv").config();
|
||||
const { notarize } = require("electron-notarize");
|
||||
|
||||
exports.default = async function notarizing(context) {
|
||||
const { electronPlatformName, appOutDir } = context;
|
||||
if (electronPlatformName !== "darwin") {
|
||||
return;
|
||||
}
|
||||
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
|
||||
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
|
||||
const appName = context.packager.appInfo.productFilename;
|
||||
return await notarize({
|
||||
appBundleId: "com.bitwarden.directory-connector",
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleId: appleId,
|
||||
appleIdPassword: appleIdPassword,
|
||||
});
|
||||
};
|
||||
21
scripts/sign.js
Normal file
21
scripts/sign.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
|
||||
exports.default = async function (configuration) {
|
||||
if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") {
|
||||
console.log(`[*] Signing file: ${configuration.path}`);
|
||||
require("child_process").execSync(
|
||||
`azuresigntool sign ` +
|
||||
`-kvu ${process.env.SIGNING_VAULT_URL} ` +
|
||||
`-kvi ${process.env.SIGNING_CLIENT_ID} ` +
|
||||
`-kvt ${process.env.SIGNING_TENANT_ID} ` +
|
||||
`-kvs ${process.env.SIGNING_CLIENT_SECRET} ` +
|
||||
`-kvc ${process.env.SIGNING_CERT_NAME} ` +
|
||||
`-fd ${configuration.hash} ` +
|
||||
`-du ${configuration.site} ` +
|
||||
`-tr http://timestamp.digicert.com ` +
|
||||
`"${configuration.path}"`,
|
||||
{
|
||||
stdio: "inherit",
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
1006
src-cli/package-lock.json
generated
Normal file
1006
src-cli/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
src-cli/package.json
Normal file
24
src-cli/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@bitwarden/directory-connector",
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"description": "Sync your user directory to your Bitwarden organization.",
|
||||
"version": "2.9.5",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
"main": "main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bitwarden/directory-connector"
|
||||
},
|
||||
"bin": {
|
||||
"bwdc": "../build-cli/bwdc.js"
|
||||
},
|
||||
"pkg": {
|
||||
"assets": "../build-cli/**/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"browser-hrtime": "^1.1.8",
|
||||
"keytar": "^7.7.0"
|
||||
}
|
||||
}
|
||||
66
src/abstractions/state.service.ts
Normal file
66
src/abstractions/state.service.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { StateService as BaseStateServiceAbstraction } from "jslib-common/abstractions/state.service";
|
||||
import { StorageOptions } from "jslib-common/models/domain/storageOptions";
|
||||
|
||||
import { DirectoryType } from "src/enums/directoryType";
|
||||
import { Account } from "src/models/account";
|
||||
import { AzureConfiguration } from "src/models/azureConfiguration";
|
||||
import { GSuiteConfiguration } from "src/models/gsuiteConfiguration";
|
||||
import { LdapConfiguration } from "src/models/ldapConfiguration";
|
||||
import { OktaConfiguration } from "src/models/oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "src/models/oneLoginConfiguration";
|
||||
import { SyncConfiguration } from "src/models/syncConfiguration";
|
||||
|
||||
export abstract class StateService extends BaseStateServiceAbstraction<Account> {
|
||||
getDirectory: <IConfiguration>(type: DirectoryType) => Promise<IConfiguration>;
|
||||
setDirectory: (
|
||||
type: DirectoryType,
|
||||
config:
|
||||
| LdapConfiguration
|
||||
| GSuiteConfiguration
|
||||
| AzureConfiguration
|
||||
| OktaConfiguration
|
||||
| OneLoginConfiguration
|
||||
) => Promise<any>;
|
||||
getLdapKey: (options?: StorageOptions) => Promise<string>;
|
||||
setLdapKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getGsuiteKey: (options?: StorageOptions) => Promise<string>;
|
||||
setGsuiteKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getAzureKey: (options?: StorageOptions) => Promise<string>;
|
||||
setAzureKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getOktaKey: (options?: StorageOptions) => Promise<string>;
|
||||
setOktaKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getOneLoginKey: (options?: StorageOptions) => Promise<string>;
|
||||
setOneLoginKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLdapConfiguration: (options?: StorageOptions) => Promise<LdapConfiguration>;
|
||||
setLdapConfiguration: (value: LdapConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getGsuiteConfiguration: (options?: StorageOptions) => Promise<GSuiteConfiguration>;
|
||||
setGsuiteConfiguration: (value: GSuiteConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getAzureConfiguration: (options?: StorageOptions) => Promise<AzureConfiguration>;
|
||||
setAzureConfiguration: (value: AzureConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getOktaConfiguration: (options?: StorageOptions) => Promise<OktaConfiguration>;
|
||||
setOktaConfiguration: (value: OktaConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getOneLoginConfiguration: (options?: StorageOptions) => Promise<OneLoginConfiguration>;
|
||||
setOneLoginConfiguration: (
|
||||
value: OneLoginConfiguration,
|
||||
options?: StorageOptions
|
||||
) => Promise<void>;
|
||||
getOrganizationId: (options?: StorageOptions) => Promise<string>;
|
||||
setOrganizationId: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSync: (options?: StorageOptions) => Promise<SyncConfiguration>;
|
||||
setSync: (value: SyncConfiguration, options?: StorageOptions) => Promise<void>;
|
||||
getDirectoryType: (options?: StorageOptions) => Promise<DirectoryType>;
|
||||
setDirectoryType: (value: DirectoryType, options?: StorageOptions) => Promise<void>;
|
||||
getUserDelta: (options?: StorageOptions) => Promise<string>;
|
||||
setUserDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastUserSync: (options?: StorageOptions) => Promise<Date>;
|
||||
setLastUserSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||
getLastGroupSync: (options?: StorageOptions) => Promise<Date>;
|
||||
setLastGroupSync: (value: Date, options?: StorageOptions) => Promise<void>;
|
||||
getGroupDelta: (options?: StorageOptions) => Promise<string>;
|
||||
setGroupDelta: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getLastSyncHash: (options?: StorageOptions) => Promise<string>;
|
||||
setLastSyncHash: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSyncingDir: (options?: StorageOptions) => Promise<boolean>;
|
||||
setSyncingDir: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
clearSyncSettings: (syncHashToo: boolean) => Promise<void>;
|
||||
}
|
||||
60
src/app/accounts/apiKey.component.html
Normal file
60
src/app/accounts/apiKey.component.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<div class="container-fluid">
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<p class="text-center font-weight-bold">{{ "welcome" | i18n }}</p>
|
||||
<p class="text-center">{{ "logInDesc" | i18n }}</p>
|
||||
<div class="card">
|
||||
<h5 class="card-header">{{ "logIn" | i18n }}</h5>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="client_id">{{ "clientId" | i18n }}</label>
|
||||
<input id="client_id" name="ClientId" [(ngModel)]="clientId" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="row-main">
|
||||
<label for="client_secret">{{ "clientSecret" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="{{ showSecret ? 'text' : 'password' }}"
|
||||
id="client_secret"
|
||||
name="ClientSecret"
|
||||
[(ngModel)]="clientSecret"
|
||||
class="form-control"
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 btn btn-link"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleSecret()"
|
||||
>
|
||||
<i
|
||||
class="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>
|
||||
103
src/app/accounts/apiKey.component.ts
Normal file
103
src/app/accounts/apiKey.component.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,61 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<form class="modal-content" (ngSubmit)="submit()">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">{{'settings' | i18n}}</h3>
|
||||
<button type="button" class="close" data-dismiss="modal" title="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h4>{{'selfHostedEnvironment' | i18n}}</h4>
|
||||
<p>{{'selfHostedEnvironmentFooter' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="baseUrl">{{'baseUrl' | i18n}}</label>
|
||||
<input id="baseUrl" type="text" name="BaseUrl" [(ngModel)]="baseUrl" class="form-control">
|
||||
<small class="text-muted form-text">{{'ex' | i18n}} https://bitwarden.company.com</small>
|
||||
</div>
|
||||
<h4>{{'customEnvironment' | i18n}}</h4>
|
||||
<p>{{'customEnvironmentFooter' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="apiUrl">{{'apiUrl' | i18n}}</label>
|
||||
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="identityUrl">{{'identityUrl' | i18n}}</label>
|
||||
<input id="identityUrl" type="text" name="IdentityUrl" [(ngModel)]="identityUrl" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-start">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-save fa-fw"></i>
|
||||
{{'save' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-dialog">
|
||||
<form class="modal-content" (ngSubmit)="submit()">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">{{ "settings" | i18n }}</h3>
|
||||
<button type="button" class="close" data-dismiss="modal" title="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h4>{{ "selfHostedEnvironment" | i18n }}</h4>
|
||||
<p>{{ "selfHostedEnvironmentFooter" | i18n }}</p>
|
||||
<div class="form-group">
|
||||
<label for="baseUrl">{{ "baseUrl" | i18n }}</label>
|
||||
<input
|
||||
id="baseUrl"
|
||||
type="text"
|
||||
name="BaseUrl"
|
||||
[(ngModel)]="baseUrl"
|
||||
class="form-control"
|
||||
/>
|
||||
<small class="text-muted form-text"
|
||||
>{{ "ex" | i18n }} https://bitwarden.company.com</small
|
||||
>
|
||||
</div>
|
||||
<h4>{{ "customEnvironment" | i18n }}</h4>
|
||||
<p>{{ "customEnvironmentFooter" | i18n }}</p>
|
||||
<div class="form-group">
|
||||
<label for="webVaultUrl">{{ "webVaultUrl" | i18n }}</label>
|
||||
<input
|
||||
id="webVaultUrl"
|
||||
type="text"
|
||||
name="WebVaultUrl"
|
||||
[(ngModel)]="webVaultUrl"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="apiUrl">{{ "apiUrl" | i18n }}</label>
|
||||
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="identityUrl">{{ "identityUrl" | i18n }}</label>
|
||||
<input
|
||||
id="identityUrl"
|
||||
type="text"
|
||||
name="IdentityUrl"
|
||||
[(ngModel)]="identityUrl"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-start">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bwi bwi-save bwi-fw"></i>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
import { EnvironmentComponent as BaseEnvironmentComponent } from 'jslib/angular/components/environment.component';
|
||||
import { EnvironmentComponent as BaseEnvironmentComponent } from "jslib-angular/components/environment.component";
|
||||
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-environment',
|
||||
templateUrl: 'environment.component.html',
|
||||
selector: "app-environment",
|
||||
templateUrl: "environment.component.html",
|
||||
})
|
||||
export class EnvironmentComponent extends BaseEnvironmentComponent {
|
||||
constructor(analytics: Angulartics2, toasterService: ToasterService,
|
||||
environmentService: EnvironmentService, i18nService: I18nService) {
|
||||
super(analytics, toasterService, environmentService, i18nService);
|
||||
}
|
||||
constructor(
|
||||
environmentService: EnvironmentService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService
|
||||
) {
|
||||
super(platformUtilsService, environmentService, i18nService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<div class="container-fluid">
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<p class="text-center font-weight-bold">{{'welcome' | i18n}}</p>
|
||||
<p class="text-center">{{'logInDesc' | i18n}}</p>
|
||||
<div class="card">
|
||||
<h5 class="card-header">{{'logIn' | i18n}}</h5>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" type="text" name="Email" [(ngModel)]="email" class="form-control" appAutoFocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="row-main">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPassword" [(ngModel)]="masterPassword" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" [disabled]="form.loading">
|
||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!form.loading"></i>
|
||||
<i class="fa fa-sign-in fa-fw" [hidden]="form.loading"></i>
|
||||
{{'logIn' | i18n}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-link" (click)="settings()">
|
||||
{{'settings' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ng-template #environment></ng-template>
|
||||
@@ -1,45 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { EnvironmentComponent } from './environment.component';
|
||||
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/login.component';
|
||||
import { ModalComponent } from 'jslib/angular/components/modal.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: 'login.component.html',
|
||||
})
|
||||
export class LoginComponent extends BaseLoginComponent {
|
||||
@ViewChild('environment', { read: ViewContainerRef }) environmentModal: ViewContainerRef;
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver) {
|
||||
super(authService, router, analytics, toasterService, i18nService);
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">{{'twoStepOptions' | i18n}}</h3>
|
||||
<button type="button" class="close" data-dismiss="modal" title="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p *ngFor="let p of providers">
|
||||
<a href="#" (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>
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
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,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, analytics, toasterService, i18nService, platformUtilsService, window);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<div class="container-fluid">
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{{title}}</h5>
|
||||
<div class="card-body">
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
|
||||
<p *ngIf="selectedProviderType === providerType.Authenticator">
|
||||
{{'enterVerificationCodeApp' | i18n}}
|
||||
</p>
|
||||
<p *ngIf="selectedProviderType === providerType.Email">
|
||||
{{'enterVerificationCodeEmail' | i18n : twoFactorEmail}}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="code">{{'verificationCode' | i18n}}</label>
|
||||
<input id="code" type="text" name="Code" [(ngModel)]="token" appAutofocus class="form-control">
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
|
||||
<p>{{'insertYubiKey' | i18n}}</p>
|
||||
<img src="../../images/yubikey.jpg" alt="">
|
||||
<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>
|
||||
@@ -1,59 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
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 { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { ModalComponent } from 'jslib/angular/components/modal.component';
|
||||
import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/components/two-factor.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-two-factor',
|
||||
templateUrl: 'two-factor.component.html',
|
||||
})
|
||||
export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver) {
|
||||
super(authService, router, analytics, toasterService, i18nService, apiService,
|
||||
platformUtilsService, window, environmentService);
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,56 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import {
|
||||
RouterModule,
|
||||
Routes,
|
||||
} from '@angular/router';
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AuthGuardService } from './services/auth-guard.service';
|
||||
import { LaunchGuardService } from './services/launch-guard.service';
|
||||
|
||||
import { LoginComponent } from './accounts/login.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 { ApiKeyComponent } from "./accounts/apiKey.component";
|
||||
import { AuthGuardService } from "./services/auth-guard.service";
|
||||
import { LaunchGuardService } from "./services/launch-guard.service";
|
||||
import { DashboardComponent } from "./tabs/dashboard.component";
|
||||
import { MoreComponent } from "./tabs/more.component";
|
||||
import { SettingsComponent } from "./tabs/settings.component";
|
||||
import { TabsComponent } from "./tabs/tabs.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: '/login', pathMatch: 'full' },
|
||||
{
|
||||
path: 'login',
|
||||
component: LoginComponent,
|
||||
canActivate: [LaunchGuardService],
|
||||
},
|
||||
{ path: '2fa', component: TwoFactorComponent },
|
||||
{
|
||||
path: 'tabs',
|
||||
component: TabsComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/tabs/dashboard',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: DashboardComponent,
|
||||
canActivate: [AuthGuardService],
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
component: SettingsComponent,
|
||||
canActivate: [AuthGuardService],
|
||||
},
|
||||
{
|
||||
path: 'more',
|
||||
component: MoreComponent,
|
||||
canActivate: [AuthGuardService],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: "", redirectTo: "/login", pathMatch: "full" },
|
||||
{
|
||||
path: "login",
|
||||
component: ApiKeyComponent,
|
||||
canActivate: [LaunchGuardService],
|
||||
},
|
||||
{
|
||||
path: "tabs",
|
||||
component: TabsComponent,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
redirectTo: "/tabs/dashboard",
|
||||
pathMatch: "full",
|
||||
},
|
||||
{
|
||||
path: "dashboard",
|
||||
component: DashboardComponent,
|
||||
canActivate: [AuthGuardService],
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
component: SettingsComponent,
|
||||
canActivate: [AuthGuardService],
|
||||
},
|
||||
{
|
||||
path: "more",
|
||||
component: MoreComponent,
|
||||
canActivate: [AuthGuardService],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
useHash: true,
|
||||
/*enableTracing: true,*/
|
||||
})],
|
||||
exports: [RouterModule],
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
useHash: true,
|
||||
/*enableTracing: true,*/
|
||||
}),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
export class AppRoutingModule {}
|
||||
|
||||
@@ -1,167 +1,160 @@
|
||||
import {
|
||||
ToasterConfig,
|
||||
ToasterContainerComponent,
|
||||
} from 'angular2-toaster';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
Component,
|
||||
NgZone,
|
||||
OnInit,
|
||||
SecurityContext,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { DomSanitizer } from "@angular/platform-browser";
|
||||
import { Router } from "@angular/router";
|
||||
import { IndividualConfig, ToastrService } from "ngx-toastr";
|
||||
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Type,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
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 { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { SyncService } from "../services/sync.service";
|
||||
|
||||
import { ModalComponent } from 'jslib/angular/components/modal.component';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.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 { ConstantsService } from 'jslib/services/constants.service';
|
||||
|
||||
import { ConfigurationService } from '../services/configuration.service';
|
||||
import { SyncService } from '../services/sync.service';
|
||||
|
||||
const BroadcasterSubscriptionId = 'AppComponent';
|
||||
const BroadcasterSubscriptionId = "AppComponent";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
styles: [],
|
||||
template: `
|
||||
<toaster-container [toasterconfig]="toasterConfig"></toaster-container>
|
||||
<ng-template #settings></ng-template>
|
||||
<router-outlet></router-outlet>`,
|
||||
selector: "app-root",
|
||||
styles: [],
|
||||
template: ` <ng-template #settings></ng-template>
|
||||
<router-outlet></router-outlet>`,
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
@ViewChild('settings', { read: ViewContainerRef }) settingsRef: ViewContainerRef;
|
||||
@ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
|
||||
|
||||
toasterConfig: ToasterConfig = new ToasterConfig({
|
||||
showCloseButton: true,
|
||||
mouseoverTimerStop: true,
|
||||
animation: 'flyRight',
|
||||
limit: 5,
|
||||
});
|
||||
constructor(
|
||||
private broadcasterService: BroadcasterService,
|
||||
private tokenService: TokenService,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private toastrService: ToastrService,
|
||||
private i18nService: I18nService,
|
||||
private sanitizer: DomSanitizer,
|
||||
private ngZone: NgZone,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private messagingService: MessagingService,
|
||||
private syncService: SyncService,
|
||||
private stateService: StateService,
|
||||
private logService: LogService
|
||||
) {}
|
||||
|
||||
private lastActivity: number = null;
|
||||
private modal: ModalComponent = null;
|
||||
ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "syncScheduleStarted":
|
||||
case "syncScheduleStopped":
|
||||
this.stateService.setSyncingDir(message.command === "syncScheduleStarted");
|
||||
break;
|
||||
case "logout":
|
||||
this.logOut(!!message.expired);
|
||||
break;
|
||||
case "checkDirSync":
|
||||
try {
|
||||
const syncConfig = await this.stateService.getSync();
|
||||
if (syncConfig.interval == null || syncConfig.interval < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
constructor(private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||
private broadcasterService: BroadcasterService, private userService: UserService,
|
||||
private tokenService: TokenService, private storageService: StorageService,
|
||||
private authService: AuthService, private router: Router, private analytics: Angulartics2,
|
||||
private toasterService: ToasterService, private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService, private ngZone: NgZone,
|
||||
private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService,
|
||||
private configurationService: ConfigurationService, private syncService: SyncService,
|
||||
private stateService: StateService, private apiService: ApiService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case 'loggedIn':
|
||||
if (await this.userService.isAuthenticated()) {
|
||||
const profile = await this.apiService.getProfile();
|
||||
this.stateService.save('profileOrganizations', profile.organizations);
|
||||
}
|
||||
break;
|
||||
case 'syncScheduleStarted':
|
||||
case 'syncScheduleStopped':
|
||||
this.stateService.save('syncingDir', message.command === 'syncScheduleStarted');
|
||||
break;
|
||||
case 'logout':
|
||||
this.logOut(!!message.expired);
|
||||
break;
|
||||
case 'checkDirSync':
|
||||
try {
|
||||
const syncConfig = await this.configurationService.getSync();
|
||||
if (syncConfig.interval == null || syncConfig.interval < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncInterval = syncConfig.interval * 60000;
|
||||
const lastGroupSync = await this.configurationService.getLastGroupSyncDate();
|
||||
const lastUserSync = await this.configurationService.getLastUserSyncDate();
|
||||
let lastSync: Date = null;
|
||||
if (lastGroupSync != null && lastUserSync == null) {
|
||||
lastSync = lastGroupSync;
|
||||
} else if (lastGroupSync == null && lastUserSync != null) {
|
||||
lastSync = lastUserSync;
|
||||
} else if (lastGroupSync != null && lastUserSync != null) {
|
||||
if (lastGroupSync.getTime() < lastUserSync.getTime()) {
|
||||
lastSync = lastGroupSync;
|
||||
} else {
|
||||
lastSync = lastUserSync;
|
||||
}
|
||||
}
|
||||
|
||||
let lastSyncAgo = syncInterval + 1;
|
||||
if (lastSync != null) {
|
||||
lastSyncAgo = new Date().getTime() - lastSync.getTime();
|
||||
}
|
||||
|
||||
if (lastSyncAgo >= syncInterval) {
|
||||
await this.syncService.sync(false, false);
|
||||
}
|
||||
} catch { }
|
||||
|
||||
this.messagingService.send('scheduleNextDirSync');
|
||||
break;
|
||||
default:
|
||||
const syncInterval = syncConfig.interval * 60000;
|
||||
const lastGroupSync = await this.stateService.getLastGroupSync();
|
||||
const lastUserSync = await this.stateService.getLastUserSync();
|
||||
let lastSync: Date = null;
|
||||
if (lastGroupSync != null && lastUserSync == null) {
|
||||
lastSync = lastGroupSync;
|
||||
} else if (lastGroupSync == null && lastUserSync != null) {
|
||||
lastSync = lastUserSync;
|
||||
} else if (lastGroupSync != null && lastUserSync != null) {
|
||||
if (lastGroupSync.getTime() < lastUserSync.getTime()) {
|
||||
lastSync = lastGroupSync;
|
||||
} else {
|
||||
lastSync = lastUserSync;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
let lastSyncAgo = syncInterval + 1;
|
||||
if (lastSync != null) {
|
||||
lastSyncAgo = new Date().getTime() - lastSync.getTime();
|
||||
}
|
||||
|
||||
private async logOut(expired: boolean) {
|
||||
const userId = await this.userService.getUserId();
|
||||
|
||||
await Promise.all([
|
||||
this.tokenService.clearToken(),
|
||||
this.userService.clear(),
|
||||
]);
|
||||
|
||||
this.authService.logOut(async () => {
|
||||
this.analytics.eventTrack.next({ action: 'Logged Out' });
|
||||
if (expired) {
|
||||
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
|
||||
this.i18nService.t('loginExpired'));
|
||||
if (lastSyncAgo >= syncInterval) {
|
||||
await this.syncService.sync(false, false);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
this.router.navigate(['login']);
|
||||
});
|
||||
}
|
||||
|
||||
private openModal<T>(type: Type<T>, ref: ViewContainerRef) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
this.messagingService.send("scheduleNextDirSync");
|
||||
break;
|
||||
case "showToast":
|
||||
this.showToast(message);
|
||||
break;
|
||||
case "ssoCallback":
|
||||
this.router.navigate(["sso"], {
|
||||
queryParams: { code: message.code, state: message.state },
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = ref.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<T>(type, ref);
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
private async logOut(expired: boolean) {
|
||||
await this.tokenService.clearToken();
|
||||
await this.stateService.clean();
|
||||
|
||||
this.authService.logOut(async () => {
|
||||
if (expired) {
|
||||
this.platformUtilsService.showToast(
|
||||
"warning",
|
||||
this.i18nService.t("loggedOut"),
|
||||
this.i18nService.t("loginExpired")
|
||||
);
|
||||
}
|
||||
this.router.navigate(["login"]);
|
||||
});
|
||||
}
|
||||
|
||||
private showToast(msg: any) {
|
||||
let message = "";
|
||||
|
||||
const options: Partial<IndividualConfig> = {};
|
||||
|
||||
if (typeof msg.text === "string") {
|
||||
message = msg.text;
|
||||
} else if (msg.text.length === 1) {
|
||||
message = msg.text[0];
|
||||
} else {
|
||||
msg.text.forEach(
|
||||
(t: string) =>
|
||||
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>")
|
||||
);
|
||||
options.enableHtml = true;
|
||||
}
|
||||
if (msg.options != null) {
|
||||
if (msg.options.trustedHtml === true) {
|
||||
options.enableHtml = true;
|
||||
}
|
||||
if (msg.options.timeout != null && msg.options.timeout > 0) {
|
||||
options.timeOut = msg.options.timeout;
|
||||
}
|
||||
}
|
||||
|
||||
this.toastrService.show(message, msg.title, options, "toast-" + msg.type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +1,43 @@
|
||||
import 'core-js';
|
||||
import 'zone.js/dist/zone';
|
||||
import "core-js/stable";
|
||||
import "zone.js/dist/zone";
|
||||
|
||||
import { ToasterModule } from 'angular2-toaster';
|
||||
import { Angulartics2Module } from 'angulartics2';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { ServicesModule } from './services/services.module';
|
||||
import { JslibModule } from "jslib-angular/jslib.module";
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
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 { AppComponent } from './app.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 { 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 { 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: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
AppRoutingModule,
|
||||
ServicesModule,
|
||||
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
|
||||
pageTracking: {
|
||||
clearQueryParams: true,
|
||||
},
|
||||
}),
|
||||
ToasterModule,
|
||||
],
|
||||
declarations: [
|
||||
ApiActionDirective,
|
||||
AppComponent,
|
||||
AutofocusDirective,
|
||||
BlurClickDirective,
|
||||
BoxRowDirective,
|
||||
DashboardComponent,
|
||||
EnvironmentComponent,
|
||||
FallbackSrcDirective,
|
||||
I18nPipe,
|
||||
IconComponent,
|
||||
LoginComponent,
|
||||
ModalComponent,
|
||||
MoreComponent,
|
||||
SearchCiphersPipe,
|
||||
SettingsComponent,
|
||||
StopClickDirective,
|
||||
StopPropDirective,
|
||||
TabsComponent,
|
||||
TwoFactorComponent,
|
||||
TwoFactorOptionsComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
EnvironmentComponent,
|
||||
ModalComponent,
|
||||
TwoFactorOptionsComponent,
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
JslibModule,
|
||||
ServicesModule,
|
||||
],
|
||||
declarations: [
|
||||
ApiKeyComponent,
|
||||
AppComponent,
|
||||
DashboardComponent,
|
||||
EnvironmentComponent,
|
||||
MoreComponent,
|
||||
SettingsComponent,
|
||||
TabsComponent,
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule { }
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { enableProdMode } from "@angular/core";
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
|
||||
import { isDev } from 'jslib/electron/utils';
|
||||
import { isDev } from "jslib-electron/utils";
|
||||
|
||||
// tslint:disable-next-line
|
||||
require('../scss/styles.scss');
|
||||
// tslint:disable-next-line
|
||||
require('../../jslib/src/misc/duo.js');
|
||||
require("../scss/styles.scss");
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
if (!isDev()) {
|
||||
enableProdMode();
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
CanActivate,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { Injectable } from "@angular/core";
|
||||
import { CanActivate } from "@angular/router";
|
||||
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuardService implements CanActivate {
|
||||
constructor(private userService: UserService, private router: Router,
|
||||
private messagingService: MessagingService) { }
|
||||
constructor(private stateService: StateService, private messagingService: MessagingService) {}
|
||||
|
||||
async canActivate() {
|
||||
const isAuthed = await this.userService.isAuthenticated();
|
||||
if (!isAuthed) {
|
||||
this.messagingService.send('logout');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
async canActivate() {
|
||||
const isAuthed = await this.stateService.getIsAuthenticated();
|
||||
if (!isAuthed) {
|
||||
this.messagingService.send("logout");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
5
src/app/services/injectionTokens.ts
Normal file
5
src/app/services/injectionTokens.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { InjectionToken } from "@angular/core";
|
||||
|
||||
export const USE_SECURE_STORAGE_FOR_SECRETS = new InjectionToken<boolean>(
|
||||
"USE_SECURE_STORAGE_FOR_SECRETS"
|
||||
);
|
||||
@@ -1,22 +1,19 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
CanActivate,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { Injectable } from "@angular/core";
|
||||
import { CanActivate, Router } from "@angular/router";
|
||||
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
|
||||
@Injectable()
|
||||
export class LaunchGuardService implements CanActivate {
|
||||
constructor(private userService: UserService, private router: Router) { }
|
||||
constructor(private stateService: StateService, private router: Router) {}
|
||||
|
||||
async canActivate() {
|
||||
const isAuthed = await this.userService.isAuthenticated();
|
||||
if (!isAuthed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.router.navigate(['/tabs/dashboard']);
|
||||
return false;
|
||||
async canActivate() {
|
||||
const isAuthed = await this.stateService.getIsAuthenticated();
|
||||
if (!isAuthed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.router.navigate(["/tabs/dashboard"]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,147 +1,189 @@
|
||||
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 {
|
||||
APP_INITIALIZER,
|
||||
NgModule,
|
||||
} from '@angular/core';
|
||||
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";
|
||||
|
||||
import { ToasterModule } from 'angular2-toaster';
|
||||
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 { ElectronLogService } from 'jslib/electron/services/electronLog.service';
|
||||
import { ElectronPlatformUtilsService } from 'jslib/electron/services/electronPlatformUtils.service';
|
||||
import { ElectronRendererMessagingService } from 'jslib/electron/services/electronRendererMessaging.service';
|
||||
import { ElectronRendererSecureStorageService } from 'jslib/electron/services/electronRendererSecureStorage.service';
|
||||
import { ElectronStorageService } from 'jslib/electron/services/electronStorage.service';
|
||||
import { isDev } from 'jslib/electron/utils';
|
||||
import { AuthGuardService } from "./auth-guard.service";
|
||||
import { USE_SECURE_STORAGE_FOR_SECRETS } from "./injectionTokens";
|
||||
import { LaunchGuardService } from "./launch-guard.service";
|
||||
|
||||
import { AuthGuardService } from './auth-guard.service';
|
||||
import { LaunchGuardService } from './launch-guard.service';
|
||||
function refreshTokenCallback(injector: Injector) {
|
||||
return () => {
|
||||
const stateService = injector.get(StateServiceAbstraction);
|
||||
const authService = injector.get(AuthServiceAbstraction);
|
||||
return refreshToken(stateService, authService);
|
||||
};
|
||||
}
|
||||
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
import { I18nService } from '../../services/i18n.service';
|
||||
import { SyncService } from '../../services/sync.service';
|
||||
export function initFactory(
|
||||
environmentService: EnvironmentServiceAbstraction,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||
stateService: StateServiceAbstraction,
|
||||
cryptoService: CryptoServiceAbstraction
|
||||
): () => Promise<void> {
|
||||
return async () => {
|
||||
await stateService.init();
|
||||
await environmentService.setUrlsFromStorage();
|
||||
await i18nService.init();
|
||||
const htmlEl = window.document.documentElement;
|
||||
htmlEl.classList.add("os_" + platformUtilsService.getDeviceString());
|
||||
htmlEl.classList.add("locale_" + i18nService.translationLocale);
|
||||
window.document.title = i18nService.t("bitwardenDirectoryConnector");
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
import { ValidationService } from 'jslib/angular/services/validation.service';
|
||||
let installAction = null;
|
||||
const installedVersion = await stateService.getInstalledVersion();
|
||||
const currentVersion = await platformUtilsService.getApplicationVersion();
|
||||
if (installedVersion == null) {
|
||||
installAction = "install";
|
||||
} else if (installedVersion !== currentVersion) {
|
||||
installAction = "update";
|
||||
}
|
||||
|
||||
import { Analytics } from 'jslib/misc/analytics';
|
||||
if (installAction != null) {
|
||||
await stateService.setInstalledVersion(currentVersion);
|
||||
}
|
||||
|
||||
import { ApiService } from 'jslib/services/api.service';
|
||||
import { AppIdService } from 'jslib/services/appId.service';
|
||||
import { AuthService } from 'jslib/services/auth.service';
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
import { ContainerService } from 'jslib/services/container.service';
|
||||
import { CryptoService } from 'jslib/services/crypto.service';
|
||||
import { EnvironmentService } from 'jslib/services/environment.service';
|
||||
import { NodeCryptoFunctionService } from 'jslib/services/nodeCryptoFunction.service';
|
||||
import { StateService } from 'jslib/services/state.service';
|
||||
import { TokenService } from 'jslib/services/token.service';
|
||||
import { UserService } from 'jslib/services/user.service';
|
||||
|
||||
import { ApiService as ApiServiceAbstraction } from 'jslib/abstractions/api.service';
|
||||
import { AppIdService as AppIdServiceAbstraction } from 'jslib/abstractions/appId.service';
|
||||
import { AuthService as AuthServiceAbstraction } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoService as CryptoServiceAbstraction } from 'jslib/abstractions/crypto.service';
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { EnvironmentService as EnvironmentServiceAbstraction } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service';
|
||||
import { LogService as LogServiceAbstraction } from 'jslib/abstractions/log.service';
|
||||
import { MessagingService as MessagingServiceAbstraction } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService as StateServiceAbstraction } from 'jslib/abstractions/state.service';
|
||||
import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/storage.service';
|
||||
import { TokenService as TokenServiceAbstraction } from 'jslib/abstractions/token.service';
|
||||
import { UserService as UserServiceAbstraction } from 'jslib/abstractions/user.service';
|
||||
|
||||
const logService = new ElectronLogService();
|
||||
const i18nService = new I18nService(window.navigator.language, './locales');
|
||||
const stateService = new StateService();
|
||||
const platformUtilsService = new ElectronPlatformUtilsService(i18nService, true);
|
||||
const broadcasterService = new BroadcasterService();
|
||||
const messagingService = new ElectronRendererMessagingService(broadcasterService);
|
||||
const storageService: StorageServiceAbstraction = new ElectronStorageService();
|
||||
const secureStorageService: StorageServiceAbstraction = new ElectronRendererSecureStorageService();
|
||||
const cryptoFunctionService: CryptoFunctionServiceAbstraction = new NodeCryptoFunctionService();
|
||||
const cryptoService = new CryptoService(storageService, secureStorageService, cryptoFunctionService);
|
||||
const appIdService = new AppIdService(storageService);
|
||||
const tokenService = new TokenService(storageService);
|
||||
const apiService = new ApiService(tokenService, platformUtilsService,
|
||||
(expired: boolean) => messagingService.send('logout', { expired: expired }));
|
||||
const environmentService = new EnvironmentService(apiService, storageService);
|
||||
const userService = new UserService(tokenService, storageService);
|
||||
const containerService = new ContainerService(cryptoService, platformUtilsService);
|
||||
const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
|
||||
i18nService, platformUtilsService, messagingService, false);
|
||||
const configurationService = new ConfigurationService(storageService, secureStorageService);
|
||||
const syncSevrice = new SyncService(configurationService, logService, cryptoFunctionService, apiService,
|
||||
messagingService, i18nService);
|
||||
|
||||
const analytics = new Analytics(window, () => true, platformUtilsService, storageService, appIdService);
|
||||
containerService.attachToWindow(window);
|
||||
environmentService.setUrlsFromStorage().then(() => {
|
||||
// Do nothing
|
||||
});
|
||||
|
||||
export function initFactory(): Function {
|
||||
return async () => {
|
||||
await i18nService.init();
|
||||
await 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');
|
||||
|
||||
let installAction = null;
|
||||
const installedVersion = await storageService.get<string>(ConstantsService.installedVersionKey);
|
||||
const currentVersion = platformUtilsService.getApplicationVersion();
|
||||
if (installedVersion == null) {
|
||||
installAction = 'install';
|
||||
} else if (installedVersion !== currentVersion) {
|
||||
installAction = 'update';
|
||||
}
|
||||
|
||||
if (installAction != null) {
|
||||
await storageService.save(ConstantsService.installedVersionKey, currentVersion);
|
||||
}
|
||||
|
||||
window.setTimeout(async () => {
|
||||
if (await userService.isAuthenticated()) {
|
||||
const profile = await apiService.getProfile();
|
||||
stateService.save('profileOrganizations', profile.organizations);
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
const containerService = new ContainerService(cryptoService);
|
||||
containerService.attachToWindow(window);
|
||||
};
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
ToasterModule,
|
||||
],
|
||||
declarations: [],
|
||||
providers: [
|
||||
ValidationService,
|
||||
AuthGuardService,
|
||||
LaunchGuardService,
|
||||
{ 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: syncSevrice },
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initFactory,
|
||||
deps: [],
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
imports: [JslibServicesModule],
|
||||
declarations: [],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initFactory,
|
||||
deps: [
|
||||
EnvironmentServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
],
|
||||
multi: true,
|
||||
},
|
||||
{ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] },
|
||||
{
|
||||
provide: I18nServiceAbstraction,
|
||||
useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"),
|
||||
deps: [WINDOW_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 {}
|
||||
|
||||
@@ -1,90 +1,110 @@
|
||||
<div class="card mb-3">
|
||||
<h3 class="card-header">{{'sync' | i18n}}</h3>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
{{'lastGroupSync' | i18n}}:
|
||||
<span *ngIf="!lastGroupSync">-</span>
|
||||
{{lastGroupSync | date:'medium'}}
|
||||
<br /> {{'lastUserSync' | i18n}}:
|
||||
<span *ngIf="!lastUserSync">-</span>
|
||||
{{lastUserSync | date:'medium'}}
|
||||
</p>
|
||||
<p>
|
||||
{{'syncStatus' | i18n}}:
|
||||
<strong *ngIf="syncRunning" class="text-success">{{'running' | i18n}}</strong>
|
||||
<strong *ngIf="!syncRunning" class="text-danger">{{'stopped' | i18n}}</strong>
|
||||
</p>
|
||||
<button #startBtn (click)="start()" [appApiAction]="startPromise" class="btn btn-primary" [disabled]="startBtn.loading">
|
||||
<i class="fa fa-play fa-fw" [hidden]="startBtn.loading"></i>
|
||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!startBtn.loading"></i>
|
||||
{{'startSync' | i18n}}
|
||||
</button>
|
||||
<button (click)="stop()" class="btn btn-primary">
|
||||
<i class="fa fa-stop fa-fw"></i>
|
||||
{{'stopSync' | i18n}}
|
||||
</button>
|
||||
<button #syncBtn (click)="sync()" [appApiAction]="syncPromise" class="btn btn-primary" [disabled]="syncBtn.loading">
|
||||
<i class="fa fa-refresh fa-fw" [ngClass]="{'fa-spin': syncBtn.loading}"></i>
|
||||
{{'syncNow' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="card-header">{{ "sync" | i18n }}</h3>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
{{ "lastGroupSync" | i18n }}:
|
||||
<span *ngIf="!lastGroupSync">-</span>
|
||||
{{ lastGroupSync | date: "medium" }}
|
||||
<br />
|
||||
{{ "lastUserSync" | i18n }}:
|
||||
<span *ngIf="!lastUserSync">-</span>
|
||||
{{ lastUserSync | date: "medium" }}
|
||||
</p>
|
||||
<p>
|
||||
{{ "syncStatus" | i18n }}:
|
||||
<strong *ngIf="syncRunning" class="text-success">{{ "running" | i18n }}</strong>
|
||||
<strong *ngIf="!syncRunning" class="text-danger">{{ "stopped" | i18n }}</strong>
|
||||
</p>
|
||||
<form #startForm [appApiAction]="startPromise" class="d-inline">
|
||||
<button (click)="start()" class="btn btn-primary" [disabled]="startForm.loading">
|
||||
<i class="bwi bwi-play bwi-fw" [hidden]="startForm.loading"></i>
|
||||
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!startForm.loading"></i>
|
||||
{{ "startSync" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<button (click)="stop()" class="btn btn-primary">
|
||||
<i class="bwi bwi-stop bwi-fw"></i>
|
||||
{{ "stopSync" | i18n }}
|
||||
</button>
|
||||
<form #syncForm [appApiAction]="syncPromise" class="d-inline">
|
||||
<button (click)="sync()" class="btn btn-primary" [disabled]="syncForm.loading">
|
||||
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': syncForm.loading }"></i>
|
||||
{{ "syncNow" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 class="card-header">{{'testing' | i18n}}</h3>
|
||||
<div class="card-body">
|
||||
<p>{{'testingDesc' | i18n}}</p>
|
||||
<button #simBtn (click)="simulate()" [appApiAction]="simPromise" class="btn btn-primary" [disabled]="simBtn.loading">
|
||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!simBtn.loading"></i>
|
||||
<i class="fa fa-bug fa-fw" [hidden]="simBtn.loading"></i>
|
||||
{{'testNow' | i18n}}
|
||||
</button>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" id="simSinceLast" [(ngModel)]="simSinceLast">
|
||||
<label class="form-check-label" for="simSinceLast">{{'testLastSync' | i18n}}</label>
|
||||
</div>
|
||||
<ng-container *ngIf="!simBtn.loading && (simUsers || simGroups)">
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<h4>{{'users' | i18n}}</h4>
|
||||
<ul class="fa-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
|
||||
<li *ngFor="let u of simEnabledUsers" title="{{u.referenceId}}">
|
||||
<i class="fa-li fa fa-user"></i>
|
||||
{{u.displayName}}
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">{{'noUsers' | i18n}}</p>
|
||||
<h4>{{'disabledUsers' | i18n}}</h4>
|
||||
<ul class="fa-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
|
||||
<li *ngFor="let u of simDisabledUsers" title="{{u.referenceId}}">
|
||||
<i class="fa-li fa fa-user"></i>
|
||||
{{u.displayName}}
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">{{'noUsers' | i18n}}</p>
|
||||
<h4>{{'deletedUsers' | i18n}}</h4>
|
||||
<ul class="fa-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
|
||||
<li *ngFor="let u of simDeletedUsers" title="{{u.referenceId}}">
|
||||
<i class="fa-li fa fa-user"></i>
|
||||
{{u.displayName}}
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">{{'noUsers' | i18n}}</p>
|
||||
</div>
|
||||
<div class="col-lg">
|
||||
<h4>{{'groups' | i18n}}</h4>
|
||||
<ul class="fa-ul testing-list" *ngIf="simGroups && simGroups.length">
|
||||
<li *ngFor="let g of simGroups" title="{{g.referenceId}}">
|
||||
<i class="fa-li fa fa-sitemap"></i>
|
||||
{{g.displayName}}
|
||||
<ul class="small" *ngIf="g.users && g.users.length">
|
||||
<li *ngFor="let u of g.users" title="{{u.referenceId}}">{{u.displayName}}</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simGroups || !simGroups.length">{{'noGroups' | i18n}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h3 class="card-header">{{ "testing" | i18n }}</h3>
|
||||
<div class="card-body">
|
||||
<p>{{ "testingDesc" | i18n }}</p>
|
||||
<form #simForm [appApiAction]="simPromise" class="d-inline">
|
||||
<button (click)="simulate()" class="btn btn-primary" [disabled]="simForm.loading">
|
||||
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!simForm.loading"></i>
|
||||
<i class="bwi bwi-bug bwi-fw" [hidden]="simForm.loading"></i>
|
||||
{{ "testNow" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="form-check mt-2">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="simSinceLast"
|
||||
[(ngModel)]="simSinceLast"
|
||||
/>
|
||||
<label class="form-check-label" for="simSinceLast">{{ "testLastSync" | i18n }}</label>
|
||||
</div>
|
||||
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<h4>{{ "users" | i18n }}</h4>
|
||||
<ul class="bwi-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
|
||||
<li *ngFor="let u of simEnabledUsers" title="{{ u.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-user"></i>
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">
|
||||
{{ "noUsers" | i18n }}
|
||||
</p>
|
||||
<h4>{{ "disabledUsers" | i18n }}</h4>
|
||||
<ul class="bwi-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
|
||||
<li *ngFor="let u of simDisabledUsers" title="{{ u.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-user"></i>
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">
|
||||
{{ "noUsers" | i18n }}
|
||||
</p>
|
||||
<h4>{{ "deletedUsers" | i18n }}</h4>
|
||||
<ul class="bwi-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
|
||||
<li *ngFor="let u of simDeletedUsers" title="{{ u.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-user"></i>
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">
|
||||
{{ "noUsers" | i18n }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-lg">
|
||||
<h4>{{ "groups" | i18n }}</h4>
|
||||
<ul class="bwi-ul testing-list" *ngIf="simGroups && simGroups.length">
|
||||
<li *ngFor="let g of simGroups" title="{{ g.referenceId }}">
|
||||
<i class="bwi bwi-li bwi-sitemap"></i>
|
||||
{{ g.displayName }}
|
||||
<ul class="small" *ngIf="g.users && g.users.length">
|
||||
<li *ngFor="let u of g.users" title="{{ u.referenceId }}">
|
||||
{{ u.displayName }}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p *ngIf="!simGroups || !simGroups.length">{{ "noGroups" | i18n }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,171 +1,124 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { GroupEntry } from "../../models/groupEntry";
|
||||
import { SimResult } from "../../models/simResult";
|
||||
import { UserEntry } from "../../models/userEntry";
|
||||
import { SyncService } from "../../services/sync.service";
|
||||
import { ConnectorUtils } from "../../utils";
|
||||
|
||||
import { AzureDirectoryService } from '../../services/azure-directory.service';
|
||||
import { GSuiteDirectoryService } from '../../services/gsuite-directory.service';
|
||||
import { LdapDirectoryService } from '../../services/ldap-directory.service';
|
||||
import { SyncService } from '../../services/sync.service';
|
||||
|
||||
import { Entry } from '../../models/entry';
|
||||
import { GroupEntry } from '../../models/groupEntry';
|
||||
import { UserEntry } from '../../models/userEntry';
|
||||
import { ConfigurationService } from '../../services/configuration.service';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
const BroadcasterSubscriptionId = 'DashboardComponent';
|
||||
const BroadcasterSubscriptionId = "DashboardComponent";
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: 'dashboard.component.html',
|
||||
selector: "app-dashboard",
|
||||
templateUrl: "dashboard.component.html",
|
||||
})
|
||||
export class DashboardComponent implements OnInit, OnDestroy {
|
||||
simGroups: GroupEntry[];
|
||||
simUsers: UserEntry[];
|
||||
simEnabledUsers: UserEntry[] = [];
|
||||
simDisabledUsers: UserEntry[] = [];
|
||||
simDeletedUsers: UserEntry[] = [];
|
||||
simPromise: Promise<any>;
|
||||
simSinceLast: boolean = false;
|
||||
syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
|
||||
startPromise: Promise<any>;
|
||||
lastGroupSync: Date;
|
||||
lastUserSync: Date;
|
||||
syncRunning: boolean;
|
||||
simGroups: GroupEntry[];
|
||||
simUsers: UserEntry[];
|
||||
simEnabledUsers: UserEntry[] = [];
|
||||
simDisabledUsers: UserEntry[] = [];
|
||||
simDeletedUsers: UserEntry[] = [];
|
||||
simPromise: Promise<SimResult>;
|
||||
simSinceLast = false;
|
||||
syncPromise: Promise<[GroupEntry[], UserEntry[]]>;
|
||||
startPromise: Promise<any>;
|
||||
lastGroupSync: Date;
|
||||
lastUserSync: Date;
|
||||
syncRunning: boolean;
|
||||
|
||||
constructor(private i18nService: I18nService, private syncService: SyncService,
|
||||
private configurationService: ConfigurationService, private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone, private messagingService: MessagingService,
|
||||
private toasterService: ToasterService, private changeDetectorRef: ChangeDetectorRef,
|
||||
private stateService: StateService) { }
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private syncService: SyncService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone,
|
||||
private messagingService: MessagingService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case 'dirSyncCompleted':
|
||||
this.updateLastSync();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
async ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "dirSyncCompleted":
|
||||
this.updateLastSync();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
|
||||
this.syncRunning = !!(await this.stateService.get('syncingDir'));
|
||||
this.updateLastSync();
|
||||
this.syncRunning = !!(await this.stateService.getSyncingDir());
|
||||
this.updateLastSync();
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.startPromise = this.syncService.sync(false, false);
|
||||
await this.startPromise;
|
||||
this.messagingService.send("scheduleNextDirSync");
|
||||
this.syncRunning = true;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingStarted"));
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.messagingService.send("cancelDirSync");
|
||||
this.syncRunning = false;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingStopped"));
|
||||
}
|
||||
|
||||
async sync() {
|
||||
this.syncPromise = this.syncService.sync(false, false);
|
||||
const result = await this.syncPromise;
|
||||
const groupCount = result[0] != null ? result[0].length : 0;
|
||||
const userCount = result[1] != null ? result[1].length : 0;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("syncCounts", groupCount.toString(), userCount.toString())
|
||||
);
|
||||
}
|
||||
|
||||
async simulate() {
|
||||
this.simGroups = [];
|
||||
this.simUsers = [];
|
||||
this.simEnabledUsers = [];
|
||||
this.simDisabledUsers = [];
|
||||
this.simDeletedUsers = [];
|
||||
|
||||
try {
|
||||
this.simPromise = ConnectorUtils.simulate(
|
||||
this.syncService,
|
||||
this.i18nService,
|
||||
this.simSinceLast
|
||||
);
|
||||
const result = await this.simPromise;
|
||||
this.simGroups = result.groups;
|
||||
this.simUsers = result.users;
|
||||
this.simEnabledUsers = result.enabledUsers;
|
||||
this.simDisabledUsers = result.disabledUsers;
|
||||
this.simDeletedUsers = result.deletedUsers;
|
||||
} catch (e) {
|
||||
this.simGroups = null;
|
||||
this.simUsers = null;
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.startPromise = this.syncService.sync(false, false);
|
||||
await this.startPromise;
|
||||
this.messagingService.send('scheduleNextDirSync');
|
||||
this.syncRunning = true;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('syncingStarted'));
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.messagingService.send('cancelDirSync');
|
||||
this.syncRunning = false;
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('syncingStopped'));
|
||||
}
|
||||
|
||||
async sync() {
|
||||
this.syncPromise = this.syncService.sync(false, false);
|
||||
const result = await this.syncPromise;
|
||||
const groupCount = result[0] != null ? result[0].length : 0;
|
||||
const userCount = result[1] != null ? result[1].length : 0;
|
||||
this.toasterService.popAsync('success', null,
|
||||
this.i18nService.t('syncCounts', groupCount.toString(), userCount.toString()));
|
||||
}
|
||||
|
||||
async simulate() {
|
||||
this.simGroups = [];
|
||||
this.simUsers = [];
|
||||
this.simEnabledUsers = [];
|
||||
this.simDisabledUsers = [];
|
||||
this.simDeletedUsers = [];
|
||||
|
||||
this.simPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const result = await this.syncService.sync(!this.simSinceLast, true);
|
||||
if (result[0] != null) {
|
||||
this.simGroups = result[0];
|
||||
}
|
||||
if (result[1] != null) {
|
||||
this.simUsers = result[1];
|
||||
}
|
||||
} catch (e) {
|
||||
this.simGroups = null;
|
||||
this.simUsers = null;
|
||||
reject(e || this.i18nService.t('syncError'));
|
||||
return;
|
||||
}
|
||||
|
||||
const userMap = new Map<string, UserEntry>();
|
||||
this.sort(this.simUsers);
|
||||
for (const u of this.simUsers) {
|
||||
userMap.set(u.externalId, u);
|
||||
if (u.deleted) {
|
||||
this.simDeletedUsers.push(u);
|
||||
} else if (u.disabled) {
|
||||
this.simDisabledUsers.push(u);
|
||||
} else {
|
||||
this.simEnabledUsers.push(u);
|
||||
}
|
||||
}
|
||||
|
||||
if (userMap.size > 0) {
|
||||
this.sort(this.simGroups);
|
||||
for (const g of this.simGroups) {
|
||||
if (g.userMemberExternalIds == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const uid of g.userMemberExternalIds) {
|
||||
if (userMap.has(uid)) {
|
||||
if ((g as any).users == null) {
|
||||
(g as any).users = [];
|
||||
}
|
||||
(g as any).users.push(userMap.get(uid));
|
||||
}
|
||||
}
|
||||
|
||||
if ((g as any).users != null) {
|
||||
this.sort((g as any).users);
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
private sort(arr: Entry[]) {
|
||||
arr.sort((a, b) => {
|
||||
return this.i18nService.collator ? this.i18nService.collator.compare(a.displayName, b.displayName) :
|
||||
a.displayName.localeCompare(b.displayName);
|
||||
});
|
||||
}
|
||||
|
||||
private async updateLastSync() {
|
||||
this.lastGroupSync = await this.configurationService.getLastGroupSyncDate();
|
||||
this.lastUserSync = await this.configurationService.getLastUserSyncDate();
|
||||
}
|
||||
private async updateLastSync() {
|
||||
this.lastGroupSync = await this.stateService.getLastGroupSync();
|
||||
this.lastUserSync = await this.stateService.getLastUserSync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="card">
|
||||
<h3 class="card-header">{{'about' | i18n}}</h3>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
{{'bitwardenDirectoryConnector' | i18n}}
|
||||
<br /> {{'version' | i18n : version}}
|
||||
<br /> © 8bit Solutions LLC 2015-{{year}}
|
||||
</p>
|
||||
<button class="btn btn-primary" type="button" (click)="update()" [disabled]="checkingForUpdate">
|
||||
<i class="fa fa-download fa-fw" [hidden]="checkingForUpdate"></i>
|
||||
<i class="fa fa-spinner fa-fw fa-spin" [hidden]="!checkingForUpdate"></i>
|
||||
{{'checkForUpdates' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="card">
|
||||
<h3 class="card-header">{{ "about" | i18n }}</h3>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
{{ "bitwardenDirectoryConnector" | i18n }}
|
||||
<br />
|
||||
{{ "version" | i18n: version }} <br />
|
||||
© Bitwarden Inc. LLC 2015-{{ year }}
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
(click)="update()"
|
||||
[disabled]="checkingForUpdate"
|
||||
>
|
||||
<i class="bwi bwi-download bwi-fw" [hidden]="checkingForUpdate"></i>
|
||||
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!checkingForUpdate"></i>
|
||||
{{ "checkForUpdates" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="card">
|
||||
<h3 class="card-header">{{'other' | i18n}}</h3>
|
||||
<div class="card-body">
|
||||
<button class="btn btn-primary" type="button" (click)="logOut()">
|
||||
{{'logOut' | i18n}}
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button" (click)="clearCache()">
|
||||
{{'clearSyncCache' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="card">
|
||||
<h3 class="card-header">{{ "other" | i18n }}</h3>
|
||||
<div class="card-body">
|
||||
<button class="btn btn-primary" type="button" (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button" (click)="clearCache()">
|
||||
{{ "clearSyncCache" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,78 +1,77 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, NgZone, OnInit } from "@angular/core";
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
|
||||
import { I18nService } from 'jslib/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';
|
||||
const BroadcasterSubscriptionId = "MoreComponent";
|
||||
|
||||
@Component({
|
||||
selector: 'app-more',
|
||||
templateUrl: 'more.component.html',
|
||||
selector: "app-more",
|
||||
templateUrl: "more.component.html",
|
||||
})
|
||||
export class MoreComponent implements OnInit {
|
||||
version: string;
|
||||
year: string;
|
||||
checkingForUpdate = false;
|
||||
version: string;
|
||||
year: string;
|
||||
checkingForUpdate = false;
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
||||
private messagingService: MessagingService, private configurationService: ConfigurationService,
|
||||
private toasterService: ToasterService, private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone, private changeDetectorRef: ChangeDetectorRef) { }
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private messagingService: MessagingService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case 'checkingForUpdate':
|
||||
this.checkingForUpdate = true;
|
||||
break;
|
||||
case 'doneCheckingForUpdate':
|
||||
this.checkingForUpdate = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
|
||||
this.year = new Date().getFullYear().toString();
|
||||
this.version = this.platformUtilsService.getApplicationVersion();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.messagingService.send('checkForUpdate');
|
||||
}
|
||||
|
||||
async logOut() {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('logOutConfirmation'), this.i18nService.t('logOut'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('cancel'));
|
||||
if (confirmed) {
|
||||
this.messagingService.send('logout');
|
||||
async ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "checkingForUpdate":
|
||||
this.checkingForUpdate = true;
|
||||
break;
|
||||
case "doneCheckingForUpdate":
|
||||
this.checkingForUpdate = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async clearCache() {
|
||||
await this.configurationService.clearStatefulSettings(true);
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('syncCacheCleared'));
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
|
||||
this.year = new Date().getFullYear().toString();
|
||||
this.version = await this.platformUtilsService.getApplicationVersion();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.messagingService.send("checkForUpdate");
|
||||
}
|
||||
|
||||
async logOut() {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t("logOutConfirmation"),
|
||||
this.i18nService.t("logOut"),
|
||||
this.i18nService.t("yes"),
|
||||
this.i18nService.t("cancel")
|
||||
);
|
||||
if (confirmed) {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
}
|
||||
|
||||
async clearCache() {
|
||||
await this.stateService.clearSyncSettings(true);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncCacheCleared"));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,136 +1,152 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
|
||||
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 { SyncConfiguration } from '../../models/syncConfiguration';
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { DirectoryType } from "../../enums/directoryType";
|
||||
import { AzureConfiguration } from "../../models/azureConfiguration";
|
||||
import { GSuiteConfiguration } from "../../models/gsuiteConfiguration";
|
||||
import { LdapConfiguration } from "../../models/ldapConfiguration";
|
||||
import { OktaConfiguration } from "../../models/oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "../../models/oneLoginConfiguration";
|
||||
import { SyncConfiguration } from "../../models/syncConfiguration";
|
||||
import { ConnectorUtils } from "../../utils";
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
templateUrl: 'settings.component.html',
|
||||
selector: "app-settings",
|
||||
templateUrl: "settings.component.html",
|
||||
})
|
||||
export class SettingsComponent implements OnInit, OnDestroy {
|
||||
directory: DirectoryType;
|
||||
directoryType = DirectoryType;
|
||||
ldap = new LdapConfiguration();
|
||||
gsuite = new GSuiteConfiguration();
|
||||
azure = new AzureConfiguration();
|
||||
okta = new OktaConfiguration();
|
||||
sync = new SyncConfiguration();
|
||||
organizationId: string;
|
||||
directoryOptions: any[];
|
||||
organizationOptions: any[];
|
||||
directory: DirectoryType;
|
||||
directoryType = DirectoryType;
|
||||
ldap = new LdapConfiguration();
|
||||
gsuite = new GSuiteConfiguration();
|
||||
azure = new AzureConfiguration();
|
||||
okta = new OktaConfiguration();
|
||||
oneLogin = new OneLoginConfiguration();
|
||||
sync = new SyncConfiguration();
|
||||
directoryOptions: any[];
|
||||
showLdapPassword = false;
|
||||
showAzureKey = false;
|
||||
showOktaKey = false;
|
||||
showOneLoginSecret = false;
|
||||
|
||||
constructor(private i18nService: I18nService, private configurationService: ConfigurationService,
|
||||
private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone,
|
||||
private stateService: StateService) {
|
||||
this.directoryOptions = [
|
||||
{ name: i18nService.t('select'), value: null },
|
||||
{ name: 'Active Directory / LDAP', value: DirectoryType.Ldap },
|
||||
{ name: 'Azure Active Directory', value: DirectoryType.AzureActiveDirectory },
|
||||
{ name: 'G Suite (Google)', value: DirectoryType.GSuite },
|
||||
{ name: 'Okta', value: DirectoryType.Okta },
|
||||
];
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private ngZone: NgZone,
|
||||
private logService: LogService,
|
||||
private stateService: StateService
|
||||
) {
|
||||
this.directoryOptions = [
|
||||
{ name: this.i18nService.t("select"), value: null },
|
||||
{ name: "Active Directory / LDAP", value: DirectoryType.Ldap },
|
||||
{ name: "Azure Active Directory", value: DirectoryType.AzureActiveDirectory },
|
||||
{ name: "G Suite (Google)", value: DirectoryType.GSuite },
|
||||
{ name: "Okta", value: DirectoryType.Okta },
|
||||
{ name: "OneLogin", value: DirectoryType.OneLogin },
|
||||
];
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.directory = await this.stateService.getDirectoryType();
|
||||
this.ldap =
|
||||
(await this.stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) || this.ldap;
|
||||
this.gsuite =
|
||||
(await this.stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
|
||||
this.gsuite;
|
||||
this.azure =
|
||||
(await this.stateService.getDirectory<AzureConfiguration>(
|
||||
DirectoryType.AzureActiveDirectory
|
||||
)) || this.azure;
|
||||
this.okta =
|
||||
(await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) || this.okta;
|
||||
this.oneLogin =
|
||||
(await this.stateService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin)) ||
|
||||
this.oneLogin;
|
||||
this.sync = (await this.stateService.getSync()) || this.sync;
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
await this.submit();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
|
||||
if (this.ldap != null && this.ldap.ad) {
|
||||
this.ldap.pagedSearch = true;
|
||||
}
|
||||
await this.stateService.setDirectoryType(this.directory);
|
||||
await this.stateService.setDirectory(DirectoryType.Ldap, this.ldap);
|
||||
await this.stateService.setDirectory(DirectoryType.GSuite, this.gsuite);
|
||||
await this.stateService.setDirectory(DirectoryType.AzureActiveDirectory, this.azure);
|
||||
await this.stateService.setDirectory(DirectoryType.Okta, this.okta);
|
||||
await this.stateService.setDirectory(DirectoryType.OneLogin, this.oneLogin);
|
||||
await this.stateService.setSync(this.sync);
|
||||
}
|
||||
|
||||
parseKeyFile() {
|
||||
const filePicker = document.getElementById("keyFile") as HTMLInputElement;
|
||||
if (filePicker.files == null || filePicker.files.length < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.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 });
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(filePicker.files[0], "utf-8");
|
||||
reader.onload = (evt) => {
|
||||
this.ngZone.run(async () => {
|
||||
try {
|
||||
const result = JSON.parse((evt.target as FileReader).result as string);
|
||||
if (result.client_email != null && result.private_key != null) {
|
||||
this.gsuite.clientEmail = result.client_email;
|
||||
this.gsuite.privateKey = result.private_key;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
|
||||
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.configurationService.getDirectory<AzureConfiguration>(
|
||||
DirectoryType.AzureActiveDirectory)) || this.azure;
|
||||
this.okta = (await this.configurationService.getDirectory<OktaConfiguration>(
|
||||
DirectoryType.Okta)) || this.okta;
|
||||
this.sync = (await this.configurationService.getSync()) || this.sync;
|
||||
// reset file input
|
||||
// ref: https://stackoverflow.com/a/20552042
|
||||
filePicker.type = "";
|
||||
filePicker.type = "file";
|
||||
filePicker.value = "";
|
||||
};
|
||||
}
|
||||
|
||||
setSslPath(id: string) {
|
||||
const filePicker = document.getElementById(id + "_file") as HTMLInputElement;
|
||||
if (filePicker.files == null || filePicker.files.length < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
await this.submit();
|
||||
}
|
||||
(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 = "";
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.ldap.ad) {
|
||||
this.sync.creationDateAttribute = 'whenCreated';
|
||||
this.sync.revisionDateAttribute = 'whenChanged';
|
||||
this.sync.emailPrefixAttribute = 'sAMAccountName';
|
||||
this.sync.groupPath = 'CN=Users';
|
||||
this.sync.userPath = 'CN=Users';
|
||||
this.sync.memberAttribute = 'member';
|
||||
this.sync.userObjectClass = 'person';
|
||||
this.sync.groupObjectClass = 'group';
|
||||
this.sync.userEmailAttribute = 'mail';
|
||||
this.sync.groupNameAttribute = 'name';
|
||||
}
|
||||
toggleLdapPassword() {
|
||||
this.showLdapPassword = !this.showLdapPassword;
|
||||
document.getElementById("password").focus();
|
||||
}
|
||||
|
||||
if (this.sync.interval != null) {
|
||||
if (this.sync.interval <= 0) {
|
||||
this.sync.interval = null;
|
||||
} else if (this.sync.interval < 5) {
|
||||
this.sync.interval = 5;
|
||||
}
|
||||
}
|
||||
toggleAzureKey() {
|
||||
this.showAzureKey = !this.showAzureKey;
|
||||
document.getElementById("secretKey").focus();
|
||||
}
|
||||
|
||||
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.saveSync(this.sync);
|
||||
}
|
||||
toggleOktaKey() {
|
||||
this.showOktaKey = !this.showOktaKey;
|
||||
document.getElementById("oktaToken").focus();
|
||||
}
|
||||
|
||||
async parseKeyFile() {
|
||||
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) => {
|
||||
this.ngZone.run(async () => {
|
||||
try {
|
||||
const result = JSON.parse((evt.target as FileReader).result);
|
||||
if (result.client_email != null && result.private_key != null) {
|
||||
this.gsuite.clientEmail = result.client_email;
|
||||
this.gsuite.privateKey = result.private_key;
|
||||
}
|
||||
} catch { }
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
|
||||
// reset file input
|
||||
// ref: https://stackoverflow.com/a/20552042
|
||||
filePicker.type = '';
|
||||
filePicker.type = 'file';
|
||||
filePicker.value = '';
|
||||
};
|
||||
}
|
||||
toggleOneLoginSecret() {
|
||||
this.showOneLoginSecret = !this.showOneLoginSecret;
|
||||
document.getElementById("oneLoginClientSecret").focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<div class="container-fluid">
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active">
|
||||
<i class="fa fa-dashboard"></i>
|
||||
{{'dashboard' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="fa fa-cogs"></i>
|
||||
{{'settings' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="more" routerLinkActive="active">
|
||||
<i class="fa fa-sliders"></i>
|
||||
{{'more' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<router-outlet></router-outlet>
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="dashboard" routerLinkActive="active">
|
||||
<i class="bwi bwi-dashboard"></i>
|
||||
{{ "dashboard" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="bwi bwi-cogs"></i>
|
||||
{{ "settings" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="more" routerLinkActive="active">
|
||||
<i class="bwi bwi-sliders"></i>
|
||||
{{ "more" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
296
src/bwdc.ts
Normal file
296
src/bwdc.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
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 { 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";
|
||||
|
||||
// eslint-disable-next-line
|
||||
const packageJson = require("./package.json");
|
||||
|
||||
export const searchService: SearchService = null;
|
||||
export class Main {
|
||||
dataFilePath: string;
|
||||
logService: ConsoleLogService;
|
||||
messagingService: NoopMessagingService;
|
||||
storageService: LowdbStorageService;
|
||||
secureStorageService: StorageServiceAbstraction;
|
||||
i18nService: I18nService;
|
||||
platformUtilsService: CliPlatformUtilsService;
|
||||
cryptoService: CryptoService;
|
||||
tokenService: TokenService;
|
||||
appIdService: AppIdService;
|
||||
apiService: NodeApiService;
|
||||
environmentService: EnvironmentService;
|
||||
containerService: ContainerService;
|
||||
cryptoFunctionService: NodeCryptoFunctionService;
|
||||
authService: AuthService;
|
||||
collectionService: CollectionService;
|
||||
cipherService: CipherService;
|
||||
fileUploadService: FileUploadService;
|
||||
folderService: FolderService;
|
||||
searchService: SearchService;
|
||||
sendService: SendService;
|
||||
settingsService: SettingsService;
|
||||
syncService: SyncService;
|
||||
passwordGenerationService: PasswordGenerationService;
|
||||
policyService: PolicyService;
|
||||
keyConnectorService: KeyConnectorService;
|
||||
program: Program;
|
||||
stateService: StateService;
|
||||
stateMigrationService: StateMigrationService;
|
||||
organizationService: OrganizationService;
|
||||
providerService: ProviderService;
|
||||
twoFactorService: TwoFactorServiceAbstraction;
|
||||
|
||||
constructor() {
|
||||
const applicationName = "Bitwarden Directory Connector";
|
||||
if (process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR) {
|
||||
this.dataFilePath = path.resolve(process.env.BITWARDENCLI_CONNECTOR_APPDATA_DIR);
|
||||
} else if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR) {
|
||||
this.dataFilePath = path.resolve(process.env.BITWARDEN_CONNECTOR_APPDATA_DIR);
|
||||
} else if (fs.existsSync(path.join(__dirname, "bitwarden-connector-appdata"))) {
|
||||
this.dataFilePath = path.join(__dirname, "bitwarden-connector-appdata");
|
||||
} else if (process.platform === "darwin") {
|
||||
this.dataFilePath = path.join(
|
||||
process.env.HOME,
|
||||
"Library/Application Support/" + applicationName
|
||||
);
|
||||
} else if (process.platform === "win32") {
|
||||
this.dataFilePath = path.join(process.env.APPDATA, applicationName);
|
||||
} else if (process.env.XDG_CONFIG_HOME) {
|
||||
this.dataFilePath = path.join(process.env.XDG_CONFIG_HOME, applicationName);
|
||||
} else {
|
||||
this.dataFilePath = path.join(process.env.HOME, ".config/" + applicationName);
|
||||
}
|
||||
|
||||
const plaintextSecrets = process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS === "true";
|
||||
this.i18nService = new I18nService("en", "./locales");
|
||||
this.platformUtilsService = new CliPlatformUtilsService(
|
||||
ClientType.DirectoryConnector,
|
||||
packageJson
|
||||
);
|
||||
this.logService = new ConsoleLogService(
|
||||
this.platformUtilsService.isDev(),
|
||||
(level) => process.env.BITWARDENCLI_CONNECTOR_DEBUG !== "true" && level <= LogLevelType.Info
|
||||
);
|
||||
this.cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
this.storageService = new LowdbStorageService(
|
||||
this.logService,
|
||||
null,
|
||||
this.dataFilePath,
|
||||
false,
|
||||
true
|
||||
);
|
||||
this.secureStorageService = plaintextSecrets
|
||||
? this.storageService
|
||||
: new KeytarSecureStorageService(applicationName);
|
||||
|
||||
this.stateMigrationService = new StateMigrationService(
|
||||
this.storageService,
|
||||
this.secureStorageService,
|
||||
new StateFactory(GlobalState, Account)
|
||||
);
|
||||
|
||||
this.stateService = new StateService(
|
||||
this.storageService,
|
||||
this.secureStorageService,
|
||||
this.logService,
|
||||
this.stateMigrationService,
|
||||
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== "true",
|
||||
new StateFactory(GlobalState, Account)
|
||||
);
|
||||
|
||||
this.cryptoService = new CryptoService(
|
||||
this.cryptoFunctionService,
|
||||
this.platformUtilsService,
|
||||
this.logService,
|
||||
this.stateService
|
||||
);
|
||||
|
||||
this.appIdService = new AppIdService(this.storageService);
|
||||
this.tokenService = new TokenService(this.stateService);
|
||||
this.messagingService = new NoopMessagingService();
|
||||
this.environmentService = new EnvironmentService(this.stateService);
|
||||
this.apiService = new NodeApiService(
|
||||
this.tokenService,
|
||||
this.platformUtilsService,
|
||||
this.environmentService,
|
||||
async (expired: boolean) => await this.logout(),
|
||||
"Bitwarden_DC/" +
|
||||
this.platformUtilsService.getApplicationVersion() +
|
||||
" (" +
|
||||
this.platformUtilsService.getDeviceString().toUpperCase() +
|
||||
")",
|
||||
(clientId, clientSecret) =>
|
||||
this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret))
|
||||
);
|
||||
this.containerService = new ContainerService(this.cryptoService);
|
||||
|
||||
this.organizationService = new OrganizationService(this.stateService);
|
||||
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.stateService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.cryptoFunctionService
|
||||
);
|
||||
|
||||
this.twoFactorService = new NoopTwoFactorService();
|
||||
|
||||
this.authService = new AuthService(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.keyConnectorService,
|
||||
this.environmentService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.i18nService
|
||||
);
|
||||
|
||||
this.syncService = new SyncService(
|
||||
this.logService,
|
||||
this.cryptoFunctionService,
|
||||
this.apiService,
|
||||
this.messagingService,
|
||||
this.i18nService,
|
||||
this.environmentService,
|
||||
this.stateService
|
||||
);
|
||||
|
||||
this.policyService = new PolicyService(
|
||||
this.stateService,
|
||||
this.organizationService,
|
||||
this.apiService
|
||||
);
|
||||
|
||||
this.passwordGenerationService = new PasswordGenerationService(
|
||||
this.cryptoService,
|
||||
this.policyService,
|
||||
this.stateService
|
||||
);
|
||||
|
||||
this.settingsService = new SettingsService(this.stateService);
|
||||
|
||||
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
|
||||
|
||||
this.cipherService = new CipherService(
|
||||
this.cryptoService,
|
||||
this.settingsService,
|
||||
this.apiService,
|
||||
this.fileUploadService,
|
||||
this.i18nService,
|
||||
() => searchService,
|
||||
this.logService,
|
||||
this.stateService
|
||||
);
|
||||
|
||||
this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService);
|
||||
|
||||
this.folderService = new FolderService(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.i18nService,
|
||||
this.cipherService,
|
||||
this.stateService
|
||||
);
|
||||
|
||||
this.collectionService = new CollectionService(
|
||||
this.cryptoService,
|
||||
this.i18nService,
|
||||
this.stateService
|
||||
);
|
||||
|
||||
this.sendService = new SendService(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.fileUploadService,
|
||||
this.i18nService,
|
||||
this.cryptoFunctionService,
|
||||
this.stateService
|
||||
);
|
||||
|
||||
this.providerService = new ProviderService(this.stateService);
|
||||
|
||||
this.program = new Program(this);
|
||||
}
|
||||
|
||||
async run() {
|
||||
await this.init();
|
||||
this.program.run();
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.tokenService.clearToken();
|
||||
await this.stateService.clean();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
await this.storageService.init();
|
||||
await this.stateService.init();
|
||||
this.containerService.attachToWindow(global);
|
||||
await this.environmentService.setUrlsFromStorage();
|
||||
// Dev Server URLs. Comment out the line above.
|
||||
// this.apiService.setUrls({
|
||||
// base: null,
|
||||
// api: 'http://localhost:4000',
|
||||
// identity: 'http://localhost:33656',
|
||||
// });
|
||||
const locale = await this.stateService.getLocale();
|
||||
await this.i18nService.init(locale);
|
||||
|
||||
const installedVersion = await this.stateService.getInstalledVersion();
|
||||
const currentVersion = await this.platformUtilsService.getApplicationVersion();
|
||||
if (installedVersion == null || installedVersion !== currentVersion) {
|
||||
await this.stateService.setInstalledVersion(currentVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const main = new Main();
|
||||
main.run();
|
||||
21
src/commands/clearCache.command.ts
Normal file
21
src/commands/clearCache.command.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
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 { StateService } from "../abstractions/state.service";
|
||||
|
||||
export class ClearCacheCommand {
|
||||
constructor(private i18nService: I18nService, private stateService: StateService) {}
|
||||
|
||||
async run(cmd: program.OptionValues): Promise<Response> {
|
||||
try {
|
||||
await this.stateService.clearSyncSettings(true);
|
||||
const res = new MessageResponse(this.i18nService.t("syncCacheCleared"), null);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/commands/config.command.ts
Normal file
152
src/commands/config.command.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
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 { StateService } from "../abstractions/state.service";
|
||||
import { DirectoryType } from "../enums/directoryType";
|
||||
import { AzureConfiguration } from "../models/azureConfiguration";
|
||||
import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
|
||||
import { LdapConfiguration } from "../models/ldapConfiguration";
|
||||
import { OktaConfiguration } from "../models/oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "../models/oneLoginConfiguration";
|
||||
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||
import { ConnectorUtils } from "../utils";
|
||||
|
||||
export class ConfigCommand {
|
||||
private directory: DirectoryType;
|
||||
private ldap = new LdapConfiguration();
|
||||
private gsuite = new GSuiteConfiguration();
|
||||
private azure = new AzureConfiguration();
|
||||
private okta = new OktaConfiguration();
|
||||
private oneLogin = new OneLoginConfiguration();
|
||||
private sync = new SyncConfiguration();
|
||||
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
private i18nService: I18nService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async run(setting: string, value: string, options: program.OptionValues): Promise<Response> {
|
||||
setting = setting.toLowerCase();
|
||||
if (value == null || value === "") {
|
||||
if (options.secretfile) {
|
||||
value = await NodeUtils.readFirstLine(options.secretfile);
|
||||
} else if (options.secretenv && process.env[options.secretenv]) {
|
||||
value = process.env[options.secretenv];
|
||||
}
|
||||
}
|
||||
try {
|
||||
switch (setting) {
|
||||
case "server":
|
||||
await this.setServer(value);
|
||||
break;
|
||||
case "directory":
|
||||
await this.setDirectory(value);
|
||||
break;
|
||||
case "ldap.password":
|
||||
await this.setLdapPassword(value);
|
||||
break;
|
||||
case "gsuite.key":
|
||||
await this.setGSuiteKey(value);
|
||||
break;
|
||||
case "azure.key":
|
||||
await this.setAzureKey(value);
|
||||
break;
|
||||
case "okta.token":
|
||||
await this.setOktaToken(value);
|
||||
break;
|
||||
case "onelogin.secret":
|
||||
await this.setOneLoginSecret(value);
|
||||
break;
|
||||
default:
|
||||
return Response.badRequest("Unknown setting.");
|
||||
}
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
const res = new MessageResponse(this.i18nService.t("savedSetting", setting), null);
|
||||
return Response.success(res);
|
||||
}
|
||||
|
||||
private async setServer(url: string) {
|
||||
url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url;
|
||||
await this.environmentService.setUrls({
|
||||
base: url,
|
||||
});
|
||||
}
|
||||
|
||||
private async setDirectory(type: string) {
|
||||
const dir = parseInt(type, null);
|
||||
if (dir < DirectoryType.Ldap || dir > DirectoryType.OneLogin) {
|
||||
throw new Error("Invalid directory type value.");
|
||||
}
|
||||
await this.loadConfig();
|
||||
this.directory = dir;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async setLdapPassword(password: string) {
|
||||
await this.loadConfig();
|
||||
this.ldap.password = password;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async setGSuiteKey(key: string) {
|
||||
await this.loadConfig();
|
||||
this.gsuite.privateKey = key != null ? key.trimLeft() : null;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async setAzureKey(key: string) {
|
||||
await this.loadConfig();
|
||||
this.azure.key = key;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async setOktaToken(token: string) {
|
||||
await this.loadConfig();
|
||||
this.okta.token = token;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async setOneLoginSecret(secret: string) {
|
||||
await this.loadConfig();
|
||||
this.oneLogin.clientSecret = secret;
|
||||
await this.saveConfig();
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
this.directory = await this.stateService.getDirectoryType();
|
||||
this.ldap =
|
||||
(await this.stateService.getDirectory<LdapConfiguration>(DirectoryType.Ldap)) || this.ldap;
|
||||
this.gsuite =
|
||||
(await this.stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
|
||||
this.gsuite;
|
||||
this.azure =
|
||||
(await this.stateService.getDirectory<AzureConfiguration>(
|
||||
DirectoryType.AzureActiveDirectory
|
||||
)) || this.azure;
|
||||
this.okta =
|
||||
(await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) || this.okta;
|
||||
this.oneLogin =
|
||||
(await this.stateService.getDirectory<OneLoginConfiguration>(DirectoryType.OneLogin)) ||
|
||||
this.oneLogin;
|
||||
this.sync = (await this.stateService.getSync()) || this.sync;
|
||||
}
|
||||
|
||||
private async saveConfig() {
|
||||
ConnectorUtils.adjustConfigForSave(this.ldap, this.sync);
|
||||
await this.stateService.setDirectoryType(this.directory);
|
||||
await this.stateService.setDirectory(DirectoryType.Ldap, this.ldap);
|
||||
await this.stateService.setDirectory(DirectoryType.GSuite, this.gsuite);
|
||||
await this.stateService.setDirectory(DirectoryType.AzureActiveDirectory, this.azure);
|
||||
await this.stateService.setDirectory(DirectoryType.Okta, this.okta);
|
||||
await this.stateService.setDirectory(DirectoryType.OneLogin, this.oneLogin);
|
||||
await this.stateService.setSync(this.sync);
|
||||
}
|
||||
}
|
||||
31
src/commands/lastSync.command.ts
Normal file
31
src/commands/lastSync.command.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Response } from "jslib-node/cli/models/response";
|
||||
import { StringResponse } from "jslib-node/cli/models/response/stringResponse";
|
||||
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
|
||||
export class LastSyncCommand {
|
||||
constructor(private stateService: StateService) {}
|
||||
|
||||
async run(object: string): Promise<Response> {
|
||||
try {
|
||||
switch (object.toLowerCase()) {
|
||||
case "groups": {
|
||||
const groupsDate = await this.stateService.getLastGroupSync();
|
||||
const groupsRes = new StringResponse(
|
||||
groupsDate == null ? null : groupsDate.toISOString()
|
||||
);
|
||||
return Response.success(groupsRes);
|
||||
}
|
||||
case "users": {
|
||||
const usersDate = await this.stateService.getLastUserSync();
|
||||
const usersRes = new StringResponse(usersDate == null ? null : usersDate.toISOString());
|
||||
return Response.success(usersRes);
|
||||
}
|
||||
default:
|
||||
return Response.badRequest("Unknown object.");
|
||||
}
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/commands/sync.command.ts
Normal file
24
src/commands/sync.command.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { Response } from "jslib-node/cli/models/response";
|
||||
import { MessageResponse } from "jslib-node/cli/models/response/messageResponse";
|
||||
|
||||
import { SyncService } from "../services/sync.service";
|
||||
|
||||
export class SyncCommand {
|
||||
constructor(private syncService: SyncService, private i18nService: I18nService) {}
|
||||
|
||||
async run(): Promise<Response> {
|
||||
try {
|
||||
const result = await this.syncService.sync(false, false);
|
||||
const groupCount = result[0] != null ? result[0].length : 0;
|
||||
const userCount = result[1] != null ? result[1].length : 0;
|
||||
const res = new MessageResponse(
|
||||
this.i18nService.t("syncingComplete"),
|
||||
this.i18nService.t("syncCounts", groupCount.toString(), userCount.toString())
|
||||
);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/commands/test.command.ts
Normal file
26
src/commands/test.command.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as program from "commander";
|
||||
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { Response } from "jslib-node/cli/models/response";
|
||||
|
||||
import { TestResponse } from "../models/response/testResponse";
|
||||
import { SyncService } from "../services/sync.service";
|
||||
import { ConnectorUtils } from "../utils";
|
||||
|
||||
export class TestCommand {
|
||||
constructor(private syncService: SyncService, private i18nService: I18nService) {}
|
||||
|
||||
async run(cmd: program.OptionValues): Promise<Response> {
|
||||
try {
|
||||
const result = await ConnectorUtils.simulate(
|
||||
this.syncService,
|
||||
this.i18nService,
|
||||
cmd.last || false
|
||||
);
|
||||
const res = new TestResponse(result);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum DirectoryType {
|
||||
Ldap = 0,
|
||||
AzureActiveDirectory = 1,
|
||||
GSuite = 2,
|
||||
Okta = 3,
|
||||
Ldap = 0,
|
||||
AzureActiveDirectory = 1,
|
||||
GSuite = 2,
|
||||
Okta = 3,
|
||||
OneLogin = 4,
|
||||
}
|
||||
|
||||
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@@ -1,2 +1,3 @@
|
||||
declare function escape(s: string): string;
|
||||
declare function unescape(s: string): string;
|
||||
declare module "duo_web_sdk";
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: *; child-src *; frame-src *; connect-src *;"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Bitwarden Directory Connector</title>
|
||||
<base href="">
|
||||
</head>
|
||||
<body>
|
||||
<base href="" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root>
|
||||
<div id="loading"><i class="fa fa-spinner fa-spin fa-3x"></i></div>
|
||||
<div id="loading"><i class="bwi bwi-spinner bwi-spin bwi-3x"></i></div>
|
||||
</app-root>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -20,12 +20,30 @@
|
||||
"emailRequired": {
|
||||
"message": "Email address is required."
|
||||
},
|
||||
"clientIdRequired": {
|
||||
"message": "Client Id is required."
|
||||
},
|
||||
"invalidClientId": {
|
||||
"message": "Invalid Client Id provided."
|
||||
},
|
||||
"clientSecretRequired": {
|
||||
"message": "Client Secret is required."
|
||||
},
|
||||
"orgApiKeyRequired": {
|
||||
"message": "Api Key must belong to an Organization"
|
||||
},
|
||||
"failedToSaveCredentials": {
|
||||
"message": "Failed to save credentials"
|
||||
},
|
||||
"invalidEmail": {
|
||||
"message": "Invalid email address."
|
||||
},
|
||||
"masterPassRequired": {
|
||||
"message": "Master password is required."
|
||||
},
|
||||
"missingRequiredInput": {
|
||||
"message": "Missing required input."
|
||||
},
|
||||
"unexpectedError": {
|
||||
"message": "An unexpected error has occurred."
|
||||
},
|
||||
@@ -129,7 +147,7 @@
|
||||
"message": "Self-hosted Environment"
|
||||
},
|
||||
"selfHostedEnvironmentFooter": {
|
||||
"message": "Specify the base URL of your on-premise hosted Bitwarden installation."
|
||||
"message": "Specify the base URL of your on-premises hosted Bitwarden installation."
|
||||
},
|
||||
"customEnvironment": {
|
||||
"message": "Custom Environment"
|
||||
@@ -140,6 +158,9 @@
|
||||
"baseUrl": {
|
||||
"message": "Server URL"
|
||||
},
|
||||
"webVaultUrl": {
|
||||
"message": "Web Vault Server URL"
|
||||
},
|
||||
"apiUrl": {
|
||||
"message": "API Server URL"
|
||||
},
|
||||
@@ -333,6 +354,9 @@
|
||||
"rootPath": {
|
||||
"message": "Root Path"
|
||||
},
|
||||
"identityAuthority": {
|
||||
"message": "Identity Authority"
|
||||
},
|
||||
"tenant": {
|
||||
"message": "Tenant"
|
||||
},
|
||||
@@ -399,6 +423,12 @@
|
||||
"groupFilter": {
|
||||
"message": "Group Filter"
|
||||
},
|
||||
"syncGroupsOneLogin": {
|
||||
"message": "Sync roles"
|
||||
},
|
||||
"groupFilterOneLogin": {
|
||||
"message": "Role Filter"
|
||||
},
|
||||
"groupObjectClass": {
|
||||
"message": "Group Object Class"
|
||||
},
|
||||
@@ -414,12 +444,55 @@
|
||||
"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"
|
||||
},
|
||||
"ldapTls": {
|
||||
"message": "Use TLS (STARTTLS)"
|
||||
},
|
||||
"ldapTlsCa": {
|
||||
"message": "Certificate CA Chain (PEM)"
|
||||
},
|
||||
"ldapSsl": {
|
||||
"message": "This server uses SSL (LDAPS)"
|
||||
"message": "Use SSL (LDAPS)"
|
||||
},
|
||||
"ldapTlsUntrustedDesc": {
|
||||
"message": "If your LDAP server uses a self-signed certificate for STARTTLS, you can configure certificate options below."
|
||||
},
|
||||
"ldapSslUntrustedDesc": {
|
||||
"message": "If your LDAPS server uses an untrusted certificate you can configure certificate options below."
|
||||
},
|
||||
"ldapSslCa": {
|
||||
"message": "Certificate CA Chain (PEM)"
|
||||
},
|
||||
"ldapSslCert": {
|
||||
"message": "Certificate (PEM)"
|
||||
},
|
||||
"ldapSslKey": {
|
||||
"message": "Certificate Private Key (PEM)"
|
||||
},
|
||||
"ldapCertDoNotVerify": {
|
||||
"message": "Do not verify server certificates (not recommended)."
|
||||
},
|
||||
"ldapAd": {
|
||||
"message": "This server uses Active Directory"
|
||||
},
|
||||
"ldapPagedResults": {
|
||||
"message": "This server pages search results"
|
||||
},
|
||||
"select": {
|
||||
"message": "Select"
|
||||
},
|
||||
@@ -501,6 +574,9 @@
|
||||
"noGroups": {
|
||||
"message": "No groups to list."
|
||||
},
|
||||
"syncingComplete": {
|
||||
"message": "Syncing complete."
|
||||
},
|
||||
"syncingStarted": {
|
||||
"message": "Syncing started."
|
||||
},
|
||||
@@ -533,7 +609,7 @@
|
||||
"message": "Welcome to the Bitwarden Directory Connector"
|
||||
},
|
||||
"logInDesc": {
|
||||
"message": "Log in as an organization admin user below."
|
||||
"message": "Log in with an organization API key below."
|
||||
},
|
||||
"dirConfigIncomplete": {
|
||||
"message": "Directory configuration incomplete."
|
||||
@@ -550,5 +626,144 @@
|
||||
},
|
||||
"hideToTray": {
|
||||
"message": "Hide to Tray"
|
||||
},
|
||||
"alwaysOnTop": {
|
||||
"message": "Always on Top",
|
||||
"description": "Application window should always stay on top of other windows"
|
||||
},
|
||||
"hideToMenuBar": {
|
||||
"message": "Hide to Menu Bar"
|
||||
},
|
||||
"savedSetting": {
|
||||
"message": "Saved setting `$SETTING_NAME$`.",
|
||||
"placeholders": {
|
||||
"setting_name": {
|
||||
"content": "$1",
|
||||
"example": "server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"largeImport": {
|
||||
"message": "More than 2000 users or groups are expected to sync."
|
||||
},
|
||||
"overwriteExisting": {
|
||||
"message": "Overwrite existing organization users based on current sync settings."
|
||||
},
|
||||
"clientId": {
|
||||
"message": "Client ID"
|
||||
},
|
||||
"clientSecret": {
|
||||
"message": "Client Secret"
|
||||
},
|
||||
"region": {
|
||||
"message": "Region"
|
||||
},
|
||||
"enterpriseSingleSignOn": {
|
||||
"message": "Enterprise Single Sign-On"
|
||||
},
|
||||
"setMasterPassword": {
|
||||
"message": "Set Master Password"
|
||||
},
|
||||
"ssoCompleteRegistration": {
|
||||
"message": "In order to complete logging in with SSO, please set a master password to access and protect your vault."
|
||||
},
|
||||
"newMasterPass": {
|
||||
"message": "New Master Password"
|
||||
},
|
||||
"confirmNewMasterPass": {
|
||||
"message": "Confirm New Master Password"
|
||||
},
|
||||
"masterPasswordPolicyInEffect": {
|
||||
"message": "One or more organization policies require your master password to meet the following requirements:"
|
||||
},
|
||||
"policyInEffectMinComplexity": {
|
||||
"message": "Minimum complexity score of $SCORE$",
|
||||
"placeholders": {
|
||||
"score": {
|
||||
"content": "$1",
|
||||
"example": "4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"policyInEffectMinLength": {
|
||||
"message": "Minimum length of $LENGTH$",
|
||||
"placeholders": {
|
||||
"length": {
|
||||
"content": "$1",
|
||||
"example": "14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"policyInEffectUppercase": {
|
||||
"message": "Contain one or more uppercase characters"
|
||||
},
|
||||
"policyInEffectLowercase": {
|
||||
"message": "Contain one or more lowercase characters"
|
||||
},
|
||||
"policyInEffectNumbers": {
|
||||
"message": "Contain one or more numbers"
|
||||
},
|
||||
"policyInEffectSpecial": {
|
||||
"message": "Contain one or more of the following special characters $CHARS$",
|
||||
"placeholders": {
|
||||
"chars": {
|
||||
"content": "$1",
|
||||
"example": "!@#$%^&*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"masterPassDesc": {
|
||||
"message": "The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it."
|
||||
},
|
||||
"reTypeMasterPass": {
|
||||
"message": "Re-type Master Password"
|
||||
},
|
||||
"masterPassHint": {
|
||||
"message": "Master Password Hint (optional)"
|
||||
},
|
||||
"masterPassHintDesc": {
|
||||
"message": "A master password hint can help you remember your password if you forget it."
|
||||
},
|
||||
"strong": {
|
||||
"message": "Strong",
|
||||
"description": "ex. A strong password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"good": {
|
||||
"message": "Good",
|
||||
"description": "ex. A good password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weak": {
|
||||
"message": "Weak",
|
||||
"description": "ex. A weak password. Scale: Weak -> Good -> Strong"
|
||||
},
|
||||
"weakMasterPassword": {
|
||||
"message": "Weak Master Password"
|
||||
},
|
||||
"weakMasterPasswordDesc": {
|
||||
"message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?"
|
||||
},
|
||||
"errorOccurred": {
|
||||
"message": "An error has occurred."
|
||||
},
|
||||
"error": {
|
||||
"message": "Error"
|
||||
},
|
||||
"masterPassLength": {
|
||||
"message": "Master password must be at least 8 characters long."
|
||||
},
|
||||
"masterPassDoesntMatch": {
|
||||
"message": "Master password confirmation does not match."
|
||||
},
|
||||
"masterPasswordPolicyRequirementsNotMet": {
|
||||
"message": "Your new master password does not meet the policy requirements."
|
||||
},
|
||||
"loading": {
|
||||
"message": "Loading"
|
||||
},
|
||||
"setMasterPasswordRedirect": {
|
||||
"message": "In order to log in with SSO from the Directory Connector, you must first log in through the web vault to set your master password."
|
||||
},
|
||||
"launchWebVault": {
|
||||
"message": "Launch Web Vault"
|
||||
}
|
||||
}
|
||||
|
||||
214
src/main.ts
214
src/main.ts
@@ -1,86 +1,152 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import * as path from 'path';
|
||||
import * as path from "path";
|
||||
|
||||
import { MenuMain } from './main/menu.main';
|
||||
import { MessagingMain } from './main/messaging.main';
|
||||
import { I18nService } from './services/i18n.service';
|
||||
import { app } from "electron";
|
||||
|
||||
import { KeytarStorageListener } from 'jslib/electron/keytarStorageListener';
|
||||
import { ElectronLogService } from 'jslib/electron/services/electronLog.service';
|
||||
import { ElectronMainMessagingService } from 'jslib/electron/services/electronMainMessaging.service';
|
||||
import { ElectronStorageService } from 'jslib/electron/services/electronStorage.service';
|
||||
import { TrayMain } from 'jslib/electron/tray.main';
|
||||
import { UpdaterMain } from 'jslib/electron/updater.main';
|
||||
import { WindowMain } from 'jslib/electron/window.main';
|
||||
import { StateFactory } from "jslib-common/factories/stateFactory";
|
||||
import { GlobalState } from "jslib-common/models/domain/globalState";
|
||||
import { KeytarStorageListener } from "jslib-electron/keytarStorageListener";
|
||||
import { ElectronLogService } from "jslib-electron/services/electronLog.service";
|
||||
import { ElectronMainMessagingService } from "jslib-electron/services/electronMainMessaging.service";
|
||||
import { ElectronStorageService } from "jslib-electron/services/electronStorage.service";
|
||||
import { TrayMain } from "jslib-electron/tray.main";
|
||||
import { UpdaterMain } from "jslib-electron/updater.main";
|
||||
import { WindowMain } from "jslib-electron/window.main";
|
||||
|
||||
import { MenuMain } from "./main/menu.main";
|
||||
import { MessagingMain } from "./main/messaging.main";
|
||||
import { Account } from "./models/account";
|
||||
import { I18nService } from "./services/i18n.service";
|
||||
import { StateService } from "./services/state.service";
|
||||
|
||||
export class Main {
|
||||
logService: ElectronLogService;
|
||||
i18nService: I18nService;
|
||||
storageService: ElectronStorageService;
|
||||
messagingService: ElectronMainMessagingService;
|
||||
keytarStorageListener: KeytarStorageListener;
|
||||
logService: ElectronLogService;
|
||||
i18nService: I18nService;
|
||||
storageService: ElectronStorageService;
|
||||
messagingService: ElectronMainMessagingService;
|
||||
keytarStorageListener: KeytarStorageListener;
|
||||
stateService: StateService;
|
||||
|
||||
windowMain: WindowMain;
|
||||
messagingMain: MessagingMain;
|
||||
menuMain: MenuMain;
|
||||
updaterMain: UpdaterMain;
|
||||
trayMain: TrayMain;
|
||||
windowMain: WindowMain;
|
||||
messagingMain: MessagingMain;
|
||||
menuMain: MenuMain;
|
||||
updaterMain: UpdaterMain;
|
||||
trayMain: TrayMain;
|
||||
|
||||
constructor() {
|
||||
// Set paths for portable builds
|
||||
let appDataPath = null;
|
||||
if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR != null) {
|
||||
appDataPath = process.env.BITWARDEN_CONNECTOR_APPDATA_DIR;
|
||||
} else if (process.platform === 'win32' && process.env.PORTABLE_EXECUTABLE_DIR != null) {
|
||||
appDataPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR, 'bitwarden-connector-appdata');
|
||||
}
|
||||
|
||||
if (appDataPath != null) {
|
||||
app.setPath('userData', appDataPath);
|
||||
}
|
||||
app.setPath('logs', path.join(app.getPath('userData'), 'logs'));
|
||||
|
||||
const args = process.argv.slice(1);
|
||||
const watch = args.some((val) => val === '--watch');
|
||||
|
||||
if (watch) {
|
||||
// tslint:disable-next-line
|
||||
require('electron-reload')(__dirname, {});
|
||||
}
|
||||
|
||||
this.logService = new ElectronLogService(null, app.getPath('userData'));
|
||||
this.i18nService = new I18nService('en', './locales/');
|
||||
this.storageService = new ElectronStorageService();
|
||||
|
||||
this.windowMain = new WindowMain(this.storageService, 800, 600);
|
||||
this.menuMain = new MenuMain(this);
|
||||
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'directory-connector', () => {
|
||||
this.messagingService.send('checkingForUpdate');
|
||||
}, null, () => {
|
||||
this.messagingService.send('doneCheckingForUpdate');
|
||||
});
|
||||
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');
|
||||
constructor() {
|
||||
// Set paths for portable builds
|
||||
let appDataPath = null;
|
||||
if (process.env.BITWARDEN_CONNECTOR_APPDATA_DIR != null) {
|
||||
appDataPath = process.env.BITWARDEN_CONNECTOR_APPDATA_DIR;
|
||||
} else if (process.platform === "win32" && process.env.PORTABLE_EXECUTABLE_DIR != null) {
|
||||
appDataPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR, "bitwarden-connector-appdata");
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
this.keytarStorageListener.init();
|
||||
this.windowMain.init().then(async () => {
|
||||
await this.i18nService.init(app.getLocale());
|
||||
this.menuMain.init();
|
||||
this.messagingMain.init();
|
||||
await this.updaterMain.init();
|
||||
await this.trayMain.init(this.i18nService.t('bitwardenDirectoryConnector'));
|
||||
}, (e: any) => {
|
||||
// tslint:disable-next-line
|
||||
console.error(e);
|
||||
});
|
||||
if (appDataPath != null) {
|
||||
app.setPath("userData", appDataPath);
|
||||
}
|
||||
app.setPath("logs", path.join(app.getPath("userData"), "logs"));
|
||||
|
||||
const args = process.argv.slice(1);
|
||||
const watch = args.some((val) => val === "--watch");
|
||||
|
||||
if (watch) {
|
||||
// eslint-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.menuMain = new MenuMain(this);
|
||||
this.updaterMain = new UpdaterMain(
|
||||
this.i18nService,
|
||||
this.windowMain,
|
||||
"directory-connector",
|
||||
() => {
|
||||
this.messagingService.send("checkingForUpdate");
|
||||
},
|
||||
() => {
|
||||
this.messagingService.send("doneCheckingForUpdate");
|
||||
},
|
||||
() => {
|
||||
this.messagingService.send("doneCheckingForUpdate");
|
||||
},
|
||||
"bitwardenDirectoryConnector"
|
||||
);
|
||||
|
||||
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.stateService);
|
||||
|
||||
this.messagingMain = new MessagingMain(
|
||||
this.windowMain,
|
||||
this.menuMain,
|
||||
this.updaterMain,
|
||||
this.trayMain
|
||||
);
|
||||
this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => {
|
||||
this.messagingMain.onMessage(message);
|
||||
});
|
||||
|
||||
this.keytarStorageListener = new KeytarStorageListener("Bitwarden Directory Connector", null);
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
this.keytarStorageListener.init();
|
||||
this.windowMain.init().then(
|
||||
async () => {
|
||||
await this.i18nService.init(app.getLocale());
|
||||
this.menuMain.init();
|
||||
this.messagingMain.init();
|
||||
await this.updaterMain.init();
|
||||
await this.trayMain.init(this.i18nService.t("bitwardenDirectoryConnector"));
|
||||
|
||||
if (!app.isDefaultProtocolClient("bwdc")) {
|
||||
app.setAsDefaultProtocolClient("bwdc");
|
||||
}
|
||||
|
||||
// Process protocol for macOS
|
||||
app.on("open-url", (event, url) => {
|
||||
event.preventDefault();
|
||||
this.processDeepLink([url]);
|
||||
});
|
||||
},
|
||||
(e: any) => {
|
||||
// eslint-disable-next-line
|
||||
console.error(e);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private processDeepLink(argv: string[]): void {
|
||||
argv
|
||||
.filter((s) => s.indexOf("bwdc://") === 0)
|
||||
.forEach((s) => {
|
||||
const url = new URL(s);
|
||||
const code = url.searchParams.get("code");
|
||||
const receivedState = url.searchParams.get("state");
|
||||
if (code != null && receivedState != null) {
|
||||
this.messagingService.send("ssoCallback", { code: code, state: receivedState });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const main = new Main();
|
||||
|
||||
@@ -1,60 +1,69 @@
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuItemConstructorOptions,
|
||||
} from 'electron';
|
||||
import { Menu, MenuItemConstructorOptions } from "electron";
|
||||
|
||||
import { Main } from '../main';
|
||||
import { BaseMenu } from "jslib-electron/baseMenu";
|
||||
|
||||
import { BaseMenu } from 'jslib/electron/baseMenu';
|
||||
import { Main } from "../main";
|
||||
|
||||
export class MenuMain extends BaseMenu {
|
||||
menu: Menu;
|
||||
menu: Menu;
|
||||
|
||||
constructor(private main: Main) {
|
||||
super(main.i18nService, main.windowMain);
|
||||
constructor(private main: Main) {
|
||||
super(main.i18nService, main.windowMain);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initProperties();
|
||||
this.initContextMenu();
|
||||
this.initApplicationMenu();
|
||||
}
|
||||
|
||||
private initApplicationMenu() {
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
this.editMenuItemOptions,
|
||||
{
|
||||
label: this.i18nService.t("view"),
|
||||
submenu: this.viewSubMenuItemOptions,
|
||||
},
|
||||
this.windowMenuItemOptions,
|
||||
];
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
const firstMenuPart: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: this.i18nService.t("aboutBitwarden"),
|
||||
role: "about",
|
||||
},
|
||||
];
|
||||
|
||||
template.unshift({
|
||||
label: this.main.i18nService.t("bitwardenDirectoryConnector"),
|
||||
submenu: firstMenuPart.concat(this.macAppMenuItemOptions),
|
||||
});
|
||||
|
||||
// Window menu
|
||||
template[template.length - 1].submenu = this.macWindowSubmenuOptions;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initProperties();
|
||||
this.initContextMenu();
|
||||
this.initApplicationMenu();
|
||||
}
|
||||
(template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(
|
||||
1,
|
||||
0,
|
||||
{
|
||||
label: this.main.i18nService.t(
|
||||
process.platform === "darwin" ? "hideToMenuBar" : "hideToTray"
|
||||
),
|
||||
click: () => this.main.messagingService.send("hideToTray"),
|
||||
accelerator: "CmdOrCtrl+Shift+M",
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
label: this.main.i18nService.t("alwaysOnTop"),
|
||||
checked: this.windowMain.win.isAlwaysOnTop(),
|
||||
click: () => this.main.windowMain.toggleAlwaysOnTop(),
|
||||
accelerator: "CmdOrCtrl+Shift+T",
|
||||
}
|
||||
);
|
||||
|
||||
private initApplicationMenu() {
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
this.editMenuItemOptions,
|
||||
{
|
||||
label: this.i18nService.t('view'),
|
||||
submenu: this.viewSubMenuItemOptions,
|
||||
},
|
||||
this.windowMenuItemOptions,
|
||||
];
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
const firstMenuPart: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: this.i18nService.t('aboutBitwarden'),
|
||||
role: 'about',
|
||||
},
|
||||
];
|
||||
|
||||
template.unshift({
|
||||
label: this.main.i18nService.t('bitwardenDirectoryConnector'),
|
||||
submenu: firstMenuPart.concat(this.macAppMenuItemOptions),
|
||||
});
|
||||
|
||||
// Window menu
|
||||
template[template.length - 1].submenu = this.macWindowSubmenuOptions;
|
||||
}
|
||||
|
||||
(template[template.length - 1].submenu as MenuItemConstructorOptions[]).splice(1, 0, {
|
||||
label: this.main.i18nService.t('hideToTray'),
|
||||
click: () => this.main.messagingService.send('hideToTray'),
|
||||
accelerator: 'CmdOrCtrl+Shift+M',
|
||||
});
|
||||
|
||||
this.menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(this.menu);
|
||||
}
|
||||
this.menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(this.menu);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,68 @@
|
||||
import {
|
||||
app,
|
||||
ipcMain,
|
||||
} from 'electron';
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { TrayMain } from 'jslib/electron/tray.main';
|
||||
import { UpdaterMain } from 'jslib/electron/updater.main';
|
||||
import { WindowMain } from 'jslib/electron/window.main';
|
||||
import { TrayMain } from "jslib-electron/tray.main";
|
||||
import { UpdaterMain } from "jslib-electron/updater.main";
|
||||
import { WindowMain } from "jslib-electron/window.main";
|
||||
|
||||
import { MenuMain } from './menu.main';
|
||||
import { MenuMain } from "./menu.main";
|
||||
|
||||
const SyncCheckInterval = 60 * 1000; // 1 minute
|
||||
|
||||
export class MessagingMain {
|
||||
private syncTimeout: NodeJS.Timer;
|
||||
private syncTimeout: NodeJS.Timer;
|
||||
|
||||
constructor(private windowMain: WindowMain, private menuMain: MenuMain,
|
||||
private updaterMain: UpdaterMain, private trayMain: TrayMain) { }
|
||||
constructor(
|
||||
private windowMain: WindowMain,
|
||||
private menuMain: MenuMain,
|
||||
private updaterMain: UpdaterMain,
|
||||
private trayMain: TrayMain
|
||||
) {}
|
||||
|
||||
init() {
|
||||
ipcMain.on('messagingService', async (event: any, message: any) => this.onMessage(message));
|
||||
}
|
||||
init() {
|
||||
ipcMain.on("messagingService", async (event: any, message: any) => this.onMessage(message));
|
||||
}
|
||||
|
||||
onMessage(message: any) {
|
||||
switch (message.command) {
|
||||
case 'checkForUpdate':
|
||||
this.updaterMain.checkForUpdate(true);
|
||||
break;
|
||||
case 'scheduleNextDirSync':
|
||||
this.scheduleNextSync();
|
||||
break;
|
||||
case 'cancelDirSync':
|
||||
this.windowMain.win.webContents.send('messagingService', {
|
||||
command: 'syncScheduleStopped',
|
||||
});
|
||||
if (this.syncTimeout) {
|
||||
global.clearTimeout(this.syncTimeout);
|
||||
}
|
||||
break;
|
||||
case 'hideToTray':
|
||||
this.trayMain.hideToTray();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleNextSync() {
|
||||
this.windowMain.win.webContents.send('messagingService', {
|
||||
command: 'syncScheduleStarted',
|
||||
onMessage(message: any) {
|
||||
switch (message.command) {
|
||||
case "checkForUpdate":
|
||||
this.updaterMain.checkForUpdate(true);
|
||||
break;
|
||||
case "scheduleNextDirSync":
|
||||
this.scheduleNextSync();
|
||||
break;
|
||||
case "cancelDirSync":
|
||||
this.windowMain.win.webContents.send("messagingService", {
|
||||
command: "syncScheduleStopped",
|
||||
});
|
||||
|
||||
if (this.syncTimeout) {
|
||||
global.clearTimeout(this.syncTimeout);
|
||||
global.clearTimeout(this.syncTimeout);
|
||||
}
|
||||
|
||||
this.syncTimeout = global.setTimeout(() => {
|
||||
if (this.windowMain.win == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.windowMain.win.webContents.send('messagingService', {
|
||||
command: 'checkDirSync',
|
||||
});
|
||||
}, SyncCheckInterval);
|
||||
break;
|
||||
case "hideToTray":
|
||||
this.trayMain.hideToTray();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleNextSync() {
|
||||
this.windowMain.win.webContents.send("messagingService", {
|
||||
command: "syncScheduleStarted",
|
||||
});
|
||||
|
||||
if (this.syncTimeout) {
|
||||
global.clearTimeout(this.syncTimeout);
|
||||
}
|
||||
|
||||
this.syncTimeout = global.setTimeout(() => {
|
||||
if (this.windowMain.win == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.windowMain.win.webContents.send("messagingService", {
|
||||
command: "checkDirSync",
|
||||
});
|
||||
}, SyncCheckInterval);
|
||||
}
|
||||
}
|
||||
|
||||
62
src/misc/logInStrategies/organizationLogIn.strategy.ts
Normal file
62
src/misc/logInStrategies/organizationLogIn.strategy.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { LogInStrategy } from "jslib-common/misc/logInStrategies/logIn.strategy";
|
||||
import { AccountKeys, AccountProfile, AccountTokens } from "jslib-common/models/domain/account";
|
||||
import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||
import { ApiLogInCredentials } from "jslib-common/models/domain/logInCredentials";
|
||||
import { ApiTokenRequest } from "jslib-common/models/request/identityToken/apiTokenRequest";
|
||||
import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse";
|
||||
|
||||
import { Account, DirectoryConfigurations, DirectorySettings } from "src/models/account";
|
||||
|
||||
export class OrganizationLogInStrategy extends LogInStrategy {
|
||||
tokenRequest: ApiTokenRequest;
|
||||
|
||||
async logIn(credentials: ApiLogInCredentials) {
|
||||
this.tokenRequest = new ApiTokenRequest(
|
||||
credentials.clientId,
|
||||
credentials.clientSecret,
|
||||
await this.buildTwoFactor(),
|
||||
await this.buildDeviceRequest()
|
||||
);
|
||||
|
||||
return this.startLogIn();
|
||||
}
|
||||
|
||||
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
|
||||
await this.saveAccountInformation(response);
|
||||
return new AuthResult();
|
||||
}
|
||||
|
||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
||||
const clientId = this.tokenRequest.clientId;
|
||||
const entityId = clientId.split("organization.")[1];
|
||||
const clientSecret = this.tokenRequest.clientSecret;
|
||||
|
||||
await this.stateService.addAccount(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: entityId,
|
||||
apiKeyClientId: clientId,
|
||||
entityId: entityId,
|
||||
},
|
||||
},
|
||||
tokens: {
|
||||
...new AccountTokens(),
|
||||
...{
|
||||
accessToken: tokenResponse.accessToken,
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
},
|
||||
},
|
||||
keys: {
|
||||
...new AccountKeys(),
|
||||
...{
|
||||
apiKeyClientSecret: clientSecret,
|
||||
},
|
||||
},
|
||||
directorySettings: new DirectorySettings(),
|
||||
directoryConfigurations: new DirectoryConfigurations(),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
2
src/models/IConfiguration.ts
Normal file
2
src/models/IConfiguration.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line
|
||||
export interface IConfiguration {}
|
||||
47
src/models/account.ts
Normal file
47
src/models/account.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Account as BaseAccount } from "jslib-common/models/domain/account";
|
||||
|
||||
import { DirectoryType } from "src/enums/directoryType";
|
||||
|
||||
import { AzureConfiguration } from "./azureConfiguration";
|
||||
import { GSuiteConfiguration } from "./gsuiteConfiguration";
|
||||
import { LdapConfiguration } from "./ldapConfiguration";
|
||||
import { OktaConfiguration } from "./oktaConfiguration";
|
||||
import { OneLoginConfiguration } from "./oneLoginConfiguration";
|
||||
import { SyncConfiguration } from "./syncConfiguration";
|
||||
|
||||
export class Account extends BaseAccount {
|
||||
directoryConfigurations?: DirectoryConfigurations = new DirectoryConfigurations();
|
||||
directorySettings: DirectorySettings = new DirectorySettings();
|
||||
clientKeys: ClientKeys = new ClientKeys();
|
||||
|
||||
constructor(init: Partial<Account>) {
|
||||
super(init);
|
||||
this.directoryConfigurations = init?.directoryConfigurations ?? new DirectoryConfigurations();
|
||||
this.directorySettings = init?.directorySettings ?? new DirectorySettings();
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientKeys {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
|
||||
export class DirectoryConfigurations {
|
||||
ldap: LdapConfiguration;
|
||||
gsuite: GSuiteConfiguration;
|
||||
azure: AzureConfiguration;
|
||||
okta: OktaConfiguration;
|
||||
oneLogin: OneLoginConfiguration;
|
||||
}
|
||||
|
||||
export class DirectorySettings {
|
||||
organizationId?: string;
|
||||
sync?: SyncConfiguration;
|
||||
directoryType?: DirectoryType;
|
||||
userDelta?: string;
|
||||
groupDelta?: string;
|
||||
lastUserSync?: Date;
|
||||
lastGroupSync?: Date;
|
||||
lastSyncHash?: string;
|
||||
syncingDir?: boolean;
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
export class AzureConfiguration {
|
||||
tenant: string;
|
||||
applicationId: string;
|
||||
key: string;
|
||||
import { IConfiguration } from "./IConfiguration";
|
||||
|
||||
export class AzureConfiguration implements IConfiguration {
|
||||
identityAuthority: string;
|
||||
tenant: string;
|
||||
applicationId: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export abstract class Entry {
|
||||
referenceId: string;
|
||||
externalId: string;
|
||||
referenceId: string;
|
||||
externalId: string;
|
||||
|
||||
get displayName(): string {
|
||||
return this.referenceId;
|
||||
}
|
||||
get displayName(): string {
|
||||
return this.referenceId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Entry } from './entry';
|
||||
import { Entry } from "./entry";
|
||||
|
||||
export class GroupEntry extends Entry {
|
||||
name: string;
|
||||
userMemberExternalIds = new Set<string>();
|
||||
groupMemberReferenceIds = new Set<string>();
|
||||
name: string;
|
||||
userMemberExternalIds = new Set<string>();
|
||||
groupMemberReferenceIds = new Set<string>();
|
||||
|
||||
get displayName(): string {
|
||||
if (this.name == null) {
|
||||
return this.referenceId;
|
||||
}
|
||||
|
||||
return this.name;
|
||||
get displayName(): string {
|
||||
if (this.name == null) {
|
||||
return this.referenceId;
|
||||
}
|
||||
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export class GSuiteConfiguration {
|
||||
clientEmail: string;
|
||||
privateKey: string;
|
||||
domain: string;
|
||||
adminUser: string;
|
||||
customer: string;
|
||||
import { IConfiguration } from "./IConfiguration";
|
||||
|
||||
export class GSuiteConfiguration implements IConfiguration {
|
||||
clientEmail: string;
|
||||
privateKey: string;
|
||||
domain: string;
|
||||
adminUser: string;
|
||||
customer: string;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { DirectoryType } from '../enums/directoryType';
|
||||
import { IConfiguration } from "./IConfiguration";
|
||||
|
||||
export class LdapConfiguration {
|
||||
ssl = false;
|
||||
hostname: string;
|
||||
port = 389;
|
||||
domain: string;
|
||||
rootPath: string;
|
||||
currentUser = false;
|
||||
username: string;
|
||||
password: string;
|
||||
ad = true;
|
||||
export class LdapConfiguration implements IConfiguration {
|
||||
ssl = false;
|
||||
startTls = false;
|
||||
tlsCaPath: string;
|
||||
sslAllowUnauthorized = false;
|
||||
sslCertPath: string;
|
||||
sslKeyPath: string;
|
||||
sslCaPath: string;
|
||||
hostname: string;
|
||||
port = 389;
|
||||
domain: string;
|
||||
rootPath: string;
|
||||
currentUser = false;
|
||||
username: string;
|
||||
password: string;
|
||||
ad = true;
|
||||
pagedSearch = true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export class OktaConfiguration {
|
||||
orgUrl: string;
|
||||
token: string;
|
||||
import { IConfiguration } from "./IConfiguration";
|
||||
|
||||
export class OktaConfiguration implements IConfiguration {
|
||||
orgUrl: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
7
src/models/oneLoginConfiguration.ts
Normal file
7
src/models/oneLoginConfiguration.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { IConfiguration } from "./IConfiguration";
|
||||
|
||||
export class OneLoginConfiguration implements IConfiguration {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
region = "us";
|
||||
}
|
||||
13
src/models/response/groupResponse.ts
Normal file
13
src/models/response/groupResponse.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { GroupEntry } from "../groupEntry";
|
||||
|
||||
export class GroupResponse {
|
||||
externalId: string;
|
||||
displayName: string;
|
||||
userIds: string[];
|
||||
|
||||
constructor(g: GroupEntry) {
|
||||
this.externalId = g.externalId;
|
||||
this.displayName = g.displayName;
|
||||
this.userIds = Array.from(g.userMemberExternalIds);
|
||||
}
|
||||
}
|
||||
25
src/models/response/testResponse.ts
Normal file
25
src/models/response/testResponse.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { BaseResponse } from "jslib-node/cli/models/response/baseResponse";
|
||||
|
||||
import { SimResult } from "../simResult";
|
||||
|
||||
import { GroupResponse } from "./groupResponse";
|
||||
import { UserResponse } from "./userResponse";
|
||||
|
||||
export class TestResponse implements BaseResponse {
|
||||
object: string;
|
||||
groups: GroupResponse[] = [];
|
||||
enabledUsers: UserResponse[] = [];
|
||||
disabledUsers: UserResponse[] = [];
|
||||
deletedUsers: UserResponse[] = [];
|
||||
|
||||
constructor(result: SimResult) {
|
||||
this.object = "test";
|
||||
this.groups = result.groups != null ? result.groups.map((g) => new GroupResponse(g)) : [];
|
||||
this.enabledUsers =
|
||||
result.enabledUsers != null ? result.enabledUsers.map((u) => new UserResponse(u)) : [];
|
||||
this.disabledUsers =
|
||||
result.disabledUsers != null ? result.disabledUsers.map((u) => new UserResponse(u)) : [];
|
||||
this.deletedUsers =
|
||||
result.deletedUsers != null ? result.deletedUsers.map((u) => new UserResponse(u)) : [];
|
||||
}
|
||||
}
|
||||
11
src/models/response/userResponse.ts
Normal file
11
src/models/response/userResponse.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { UserEntry } from "../userEntry";
|
||||
|
||||
export class UserResponse {
|
||||
externalId: string;
|
||||
displayName: string;
|
||||
|
||||
constructor(u: UserEntry) {
|
||||
this.externalId = u.externalId;
|
||||
this.displayName = u.displayName;
|
||||
}
|
||||
}
|
||||
10
src/models/simResult.ts
Normal file
10
src/models/simResult.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { GroupEntry } from "./groupEntry";
|
||||
import { UserEntry } from "./userEntry";
|
||||
|
||||
export class SimResult {
|
||||
groups: GroupEntry[] = [];
|
||||
users: UserEntry[] = [];
|
||||
enabledUsers: UserEntry[] = [];
|
||||
disabledUsers: UserEntry[] = [];
|
||||
deletedUsers: UserEntry[] = [];
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
export class SyncConfiguration {
|
||||
users = false;
|
||||
groups = false;
|
||||
interval = 5;
|
||||
userFilter: string;
|
||||
groupFilter: string;
|
||||
removeDisabled = false;
|
||||
// Ldap properties
|
||||
groupObjectClass: string;
|
||||
userObjectClass: string;
|
||||
groupPath: string;
|
||||
userPath: string;
|
||||
groupNameAttribute: string;
|
||||
userEmailAttribute: string;
|
||||
memberAttribute: string;
|
||||
useEmailPrefixSuffix = false;
|
||||
emailPrefixAttribute: string;
|
||||
emailSuffix: string;
|
||||
creationDateAttribute: string;
|
||||
revisionDateAttribute: string;
|
||||
users = false;
|
||||
groups = false;
|
||||
interval = 5;
|
||||
userFilter: string;
|
||||
groupFilter: string;
|
||||
removeDisabled = false;
|
||||
overwriteExisting = false;
|
||||
largeImport = false;
|
||||
// Ldap properties
|
||||
groupObjectClass: string;
|
||||
userObjectClass: string;
|
||||
groupPath: string;
|
||||
userPath: string;
|
||||
groupNameAttribute: string;
|
||||
userEmailAttribute: string;
|
||||
memberAttribute: string;
|
||||
useEmailPrefixSuffix = false;
|
||||
emailPrefixAttribute: string;
|
||||
emailSuffix: string;
|
||||
creationDateAttribute: string;
|
||||
revisionDateAttribute: string;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Entry } from './entry';
|
||||
import { Entry } from "./entry";
|
||||
|
||||
export class UserEntry extends Entry {
|
||||
email: string;
|
||||
disabled = false;
|
||||
deleted = false;
|
||||
email: string;
|
||||
disabled = false;
|
||||
deleted = false;
|
||||
|
||||
get displayName(): string {
|
||||
if (this.email == null) {
|
||||
return this.referenceId;
|
||||
}
|
||||
|
||||
return this.email;
|
||||
get displayName(): string {
|
||||
if (this.email == null) {
|
||||
return this.referenceId;
|
||||
}
|
||||
|
||||
return this.email;
|
||||
}
|
||||
}
|
||||
|
||||
1849
src/package-lock.json
generated
1849
src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,22 @@
|
||||
{
|
||||
"name": "bitwarden-directory-connector",
|
||||
"name": "@bitwarden/directory-connector",
|
||||
"productName": "Bitwarden Directory Connector",
|
||||
"description": "Sync your user directory to your Bitwarden organization.",
|
||||
"version": "2.0.0",
|
||||
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"version": "2.10.0",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
"main": "main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bitwarden/desktop"
|
||||
"url": "https://github.com/bitwarden/directory-connector"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-log": "2.2.14",
|
||||
"electron-store": "1.3.0",
|
||||
"electron-updater": "2.21.4",
|
||||
"keytar": "4.1.0"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
319
src/program.ts
Normal file
319
src/program.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import * as path from "path";
|
||||
|
||||
import * as chalk from "chalk";
|
||||
import * as program from "commander";
|
||||
|
||||
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 { 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";
|
||||
|
||||
const writeLn = (s: string, finalLine = false, error = false) => {
|
||||
const stream = error ? process.stderr : process.stdout;
|
||||
if (finalLine && process.platform === "win32") {
|
||||
stream.write(s);
|
||||
} else {
|
||||
stream.write(s + "\n");
|
||||
}
|
||||
};
|
||||
|
||||
export class Program extends BaseProgram {
|
||||
constructor(private main: Main) {
|
||||
super(main.stateService, 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");
|
||||
|
||||
program.on("option:pretty", () => {
|
||||
process.env.BW_PRETTY = "true";
|
||||
});
|
||||
|
||||
program.on("option:raw", () => {
|
||||
process.env.BW_RAW = "true";
|
||||
});
|
||||
|
||||
program.on("option:quiet", () => {
|
||||
process.env.BW_QUIET = "true";
|
||||
});
|
||||
|
||||
program.on("option:response", () => {
|
||||
process.env.BW_RESPONSE = "true";
|
||||
});
|
||||
|
||||
program.on("option:cleanexit", () => {
|
||||
process.env.BW_CLEANEXIT = "true";
|
||||
});
|
||||
|
||||
program.on("option:nointeraction", () => {
|
||||
process.env.BW_NOINTERACTION = "true";
|
||||
});
|
||||
|
||||
program.on("command:*", () => {
|
||||
writeLn(chalk.redBright("Invalid command: " + program.args.join(" ")), false, true);
|
||||
writeLn("See --help for a list of available commands.", true, true);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
program.on("--help", () => {
|
||||
writeLn("\n Examples:");
|
||||
writeLn("");
|
||||
writeLn(" bwdc login");
|
||||
writeLn(" bwdc test");
|
||||
writeLn(" bwdc sync");
|
||||
writeLn(" bwdc last-sync");
|
||||
writeLn(" bwdc config server https://bw.company.com");
|
||||
writeLn(" bwdc update");
|
||||
writeLn("", true);
|
||||
});
|
||||
|
||||
program
|
||||
.command("login [clientId] [clientSecret]")
|
||||
.description("Log into an organization account.", {
|
||||
clientId: "Client_id part of your organization's API key",
|
||||
clientSecret: "Client_secret part of your organization's API key",
|
||||
})
|
||||
.action(async (clientId: string, clientSecret: string, options: program.OptionValues) => {
|
||||
await this.exitIfAuthed();
|
||||
const command = new LoginCommand(
|
||||
this.main.authService,
|
||||
this.main.apiService,
|
||||
this.main.i18nService,
|
||||
this.main.environmentService,
|
||||
this.main.passwordGenerationService,
|
||||
this.main.cryptoFunctionService,
|
||||
this.main.platformUtilsService,
|
||||
this.main.stateService,
|
||||
this.main.cryptoService,
|
||||
this.main.policyService,
|
||||
this.main.twoFactorService,
|
||||
"connector"
|
||||
);
|
||||
|
||||
if (!Utils.isNullOrWhitespace(clientId)) {
|
||||
process.env.BW_CLIENTID = clientId;
|
||||
}
|
||||
if (!Utils.isNullOrWhitespace(clientSecret)) {
|
||||
process.env.BW_CLIENTSECRET = clientSecret;
|
||||
}
|
||||
|
||||
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
|
||||
const response = await command.run(null, null, options);
|
||||
this.processResponse(response);
|
||||
});
|
||||
|
||||
program
|
||||
.command("logout")
|
||||
.description("Log out of the current user account.")
|
||||
.on("--help", () => {
|
||||
writeLn("\n Examples:");
|
||||
writeLn("");
|
||||
writeLn(" bwdc logout");
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(async () => {
|
||||
await this.exitIfNotAuthed();
|
||||
const command = new LogoutCommand(
|
||||
this.main.authService,
|
||||
this.main.i18nService,
|
||||
async () => await this.main.logout()
|
||||
);
|
||||
const response = await command.run();
|
||||
this.processResponse(response);
|
||||
});
|
||||
|
||||
program
|
||||
.command("test")
|
||||
.description("Test a simulated sync.")
|
||||
.option("-l, --last", "Since the last successful sync.")
|
||||
.on("--help", () => {
|
||||
writeLn("\n Examples:");
|
||||
writeLn("");
|
||||
writeLn(" bwdc test");
|
||||
writeLn(" bwdc test --last");
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(async (options: program.OptionValues) => {
|
||||
await this.exitIfNotAuthed();
|
||||
const command = new TestCommand(this.main.syncService, this.main.i18nService);
|
||||
const response = await command.run(options);
|
||||
this.processResponse(response);
|
||||
});
|
||||
|
||||
program
|
||||
.command("sync")
|
||||
.description("Sync the directory.")
|
||||
.on("--help", () => {
|
||||
writeLn("\n Examples:");
|
||||
writeLn("");
|
||||
writeLn(" bwdc sync");
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(async () => {
|
||||
await this.exitIfNotAuthed();
|
||||
const command = new SyncCommand(this.main.syncService, this.main.i18nService);
|
||||
const response = await command.run();
|
||||
this.processResponse(response);
|
||||
});
|
||||
|
||||
program
|
||||
.command("last-sync <object>")
|
||||
.description("Get the last successful sync date.")
|
||||
.on("--help", () => {
|
||||
writeLn("\n Notes:");
|
||||
writeLn("");
|
||||
writeLn(" Returns empty response if no sync has been performed for the given object.");
|
||||
writeLn("");
|
||||
writeLn(" Examples:");
|
||||
writeLn("");
|
||||
writeLn(" bwdc last-sync groups");
|
||||
writeLn(" bwdc last-sync users");
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(async (object: string) => {
|
||||
await this.exitIfNotAuthed();
|
||||
const command = new LastSyncCommand(this.main.stateService);
|
||||
const response = await command.run(object);
|
||||
this.processResponse(response);
|
||||
});
|
||||
|
||||
program
|
||||
.command("config <setting> [value]")
|
||||
.description("Configure settings.")
|
||||
.option("--secretenv <variable-name>", "Read secret from the named environment variable.")
|
||||
.option("--secretfile <filename>", "Read secret from first line of the named file.")
|
||||
.on("--help", () => {
|
||||
writeLn("\n Settings:");
|
||||
writeLn("");
|
||||
writeLn(" server - On-premise hosted installation URL.");
|
||||
writeLn(" directory - The type of directory to use.");
|
||||
writeLn(" ldap.password - The password for connection to this LDAP server.");
|
||||
writeLn(" azure.key - The Azure AD secret key.");
|
||||
writeLn(" gsuite.key - The G Suite private key.");
|
||||
writeLn(" okta.token - The Okta token.");
|
||||
writeLn(" onelogin.secret - The OneLogin client secret.");
|
||||
writeLn("");
|
||||
writeLn(" Examples:");
|
||||
writeLn("");
|
||||
writeLn(" bwdc config server https://bw.company.com");
|
||||
writeLn(" bwdc config server bitwarden.com");
|
||||
writeLn(" bwdc config directory 1");
|
||||
writeLn(" bwdc config ldap.password <password>");
|
||||
writeLn(" bwdc config ldap.password --secretenv LDAP_PWD");
|
||||
writeLn(" bwdc config azure.key <key>");
|
||||
writeLn(" bwdc config gsuite.key <key>");
|
||||
writeLn(" bwdc config okta.token <token>");
|
||||
writeLn(" bwdc config onelogin.secret <secret>");
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(async (setting: string, value: string, options: program.OptionValues) => {
|
||||
const command = new ConfigCommand(
|
||||
this.main.environmentService,
|
||||
this.main.i18nService,
|
||||
this.main.stateService
|
||||
);
|
||||
const response = await command.run(setting, value, options);
|
||||
this.processResponse(response);
|
||||
});
|
||||
|
||||
program
|
||||
.command("data-file")
|
||||
.description("Path to data.json database file.")
|
||||
.on("--help", () => {
|
||||
writeLn("\n Examples:");
|
||||
writeLn("");
|
||||
writeLn(" bwdc data-file");
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(() => {
|
||||
this.processResponse(
|
||||
Response.success(new StringResponse(path.join(this.main.dataFilePath, "data.json")))
|
||||
);
|
||||
});
|
||||
|
||||
program
|
||||
.command("clear-cache")
|
||||
.description("Clear the sync cache.")
|
||||
.on("--help", () => {
|
||||
writeLn("\n Examples:");
|
||||
writeLn("");
|
||||
writeLn(" bwdc clear-cache");
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(async (options: program.OptionValues) => {
|
||||
const command = new ClearCacheCommand(this.main.i18nService, this.main.stateService);
|
||||
const response = await command.run(options);
|
||||
this.processResponse(response);
|
||||
});
|
||||
|
||||
program
|
||||
.command("update")
|
||||
.description("Check for updates.")
|
||||
.on("--help", () => {
|
||||
writeLn("\n Notes:");
|
||||
writeLn("");
|
||||
writeLn(" Returns the URL to download the newest version of this CLI tool.");
|
||||
writeLn("");
|
||||
writeLn(" Use the `--raw` option to return only the download URL for the update.");
|
||||
writeLn("");
|
||||
writeLn(" Examples:");
|
||||
writeLn("");
|
||||
writeLn(" bwdc update");
|
||||
writeLn(" bwdc update --raw");
|
||||
writeLn("", true);
|
||||
})
|
||||
.action(async () => {
|
||||
const command = new UpdateCommand(
|
||||
this.main.platformUtilsService,
|
||||
this.main.i18nService,
|
||||
"directory-connector",
|
||||
"bwdc",
|
||||
false
|
||||
);
|
||||
const response = await command.run();
|
||||
this.processResponse(response);
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
if (process.argv.slice(2).length === 0) {
|
||||
program.outputHelp();
|
||||
}
|
||||
}
|
||||
|
||||
async exitIfAuthed() {
|
||||
const authed = await this.stateService.getIsAuthenticated();
|
||||
if (authed) {
|
||||
const type = await this.stateService.getEntityType();
|
||||
const id = await this.stateService.getEntityId();
|
||||
this.processResponse(
|
||||
Response.error("You are already logged in as " + type + "." + id + "."),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async exitIfNotAuthed() {
|
||||
const authed = await this.stateService.getIsAuthenticated();
|
||||
if (!authed) {
|
||||
this.processResponse(Response.error("You are not logged in."), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/scss/bootstrap.scss
vendored
23
src/scss/bootstrap.scss
vendored
@@ -1,5 +1,15 @@
|
||||
$theme-colors: ( "primary": #3c8dbc, "primary-accent": #286090, "danger": #dd4b39, "success": #00a65a, "info": #555555, "warning": #bf7e16);
|
||||
$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
$theme-colors: (
|
||||
"primary": #175ddc,
|
||||
"primary-accent": #1252a3,
|
||||
"danger": #dd4b39,
|
||||
"success": #00a65a,
|
||||
"info": #555555,
|
||||
"warning": #bf7e16,
|
||||
"secondary": #ced4da,
|
||||
"secondary-alt": #1a3b66,
|
||||
);
|
||||
$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
||||
$h1-font-size: 2rem;
|
||||
$h2-font-size: 1.3rem;
|
||||
@@ -8,4 +18,13 @@ $h4-font-size: 1rem;
|
||||
$h5-font-size: 1rem;
|
||||
$h6-font-size: 1rem;
|
||||
|
||||
$primary: map_get($theme-colors, "primary");
|
||||
$primary-accent: map_get($theme-colors, "primary-accent");
|
||||
$success: map_get($theme-colors, "success");
|
||||
$info: map_get($theme-colors, "info");
|
||||
$warning: map_get($theme-colors, "warning");
|
||||
$danger: map_get($theme-colors, "danger");
|
||||
$secondary: map_get($theme-colors, "secondary");
|
||||
$secondary-alt: map_get($theme-colors, "secondary-alt");
|
||||
|
||||
@import "~bootstrap/scss/bootstrap.scss";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import "~bootstrap/scss/_variables.scss";
|
||||
|
||||
html.os_windows {
|
||||
body {
|
||||
border-top: 1px solid $gray-400;
|
||||
}
|
||||
body {
|
||||
border-top: 1px solid $gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,143 @@
|
||||
@import "~bootstrap/scss/_variables.scss";
|
||||
|
||||
body {
|
||||
padding: 10px 0 20px 0;
|
||||
padding: 10px 0 20px 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: 20px;
|
||||
|
||||
small {
|
||||
color: $text-muted;
|
||||
font-size: $h1-font-size * .5;
|
||||
}
|
||||
small {
|
||||
color: $text-muted;
|
||||
font-size: $h1-font-size * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
text-transform: uppercase;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#duo-frame {
|
||||
background: url('../images/loading.svg') 0 0 no-repeat;
|
||||
height: 380px;
|
||||
background: url("../images/loading.svg") 0 0 no-repeat;
|
||||
height: 380px;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
app-root > #loading {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: $text-muted;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
ul.testing-list {
|
||||
ul {
|
||||
padding-left: 18px;
|
||||
}
|
||||
ul {
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
li.deleted {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
li.deleted {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.callout {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #000000;
|
||||
border-left-width: 5px;
|
||||
border-radius: 3px;
|
||||
border-color: #ddd;
|
||||
background-color: white;
|
||||
|
||||
.callout-heading {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h3.callout-heading {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&.callout-primary {
|
||||
border-left-color: $primary;
|
||||
|
||||
.callout-heading {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.callout-info {
|
||||
border-left-color: $info;
|
||||
|
||||
.callout-heading {
|
||||
color: $info;
|
||||
}
|
||||
}
|
||||
|
||||
&.callout-danger {
|
||||
border-left-color: $danger;
|
||||
|
||||
.callout-heading {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
&.callout-success {
|
||||
border-left-color: $success;
|
||||
|
||||
.callout-heading {
|
||||
color: $success;
|
||||
}
|
||||
}
|
||||
|
||||
&.callout-warning {
|
||||
border-left-color: $warning;
|
||||
|
||||
.callout-heading {
|
||||
color: $warning;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,120 +1,126 @@
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome.scss";
|
||||
@import "~angular2-toaster/toaster";
|
||||
@import "~ngx-toastr/toastr";
|
||||
|
||||
@import "~bootstrap/scss/_variables.scss";
|
||||
|
||||
#toast-container {
|
||||
.toast-container {
|
||||
.toast-close-button {
|
||||
font-size: 18px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.ngx-toastr {
|
||||
align-items: center;
|
||||
background-image: none !important;
|
||||
border-radius: $border-radius;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
|
||||
.toast-close-button {
|
||||
right: -0.15em;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.toast {
|
||||
opacity: 1 !important;
|
||||
background-image: none !important;
|
||||
border-radius: $border-radius;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&:before {
|
||||
font-family: FontAwesome;
|
||||
font-size: 25px;
|
||||
line-height: 20px;
|
||||
float: left;
|
||||
color: #ffffff;
|
||||
padding-right: 10px;
|
||||
margin: auto 0 auto -36px;
|
||||
}
|
||||
|
||||
.toaster-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.toast-danger, &.toast-error {
|
||||
background-image: none !important;
|
||||
background-color: $danger;
|
||||
|
||||
&:before {
|
||||
content: "\f0e7";
|
||||
margin-left: -30px;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-warning {
|
||||
background-image: none !important;
|
||||
background-color: $warning;
|
||||
|
||||
&:before {
|
||||
content: "\f071";
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-info {
|
||||
background-image: none !important;
|
||||
background-color: $info;
|
||||
|
||||
&:before {
|
||||
content: "\f05a";
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-success {
|
||||
background-image: none !important;
|
||||
background-color: $success;
|
||||
|
||||
&:before {
|
||||
content: "\f00C";
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.icon i::before {
|
||||
float: left;
|
||||
font-style: normal;
|
||||
font-family: $icomoon-font-family;
|
||||
font-size: 25px;
|
||||
line-height: 20px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
p {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-danger,
|
||||
&.toast-error {
|
||||
background-color: $danger;
|
||||
|
||||
.icon i::before {
|
||||
content: map_get($icons, "error");
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-warning {
|
||||
background-color: $warning;
|
||||
|
||||
.icon i::before {
|
||||
content: map_get($icons, "exclamation-triangle");
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-info {
|
||||
background-color: $info;
|
||||
|
||||
.icon i:before {
|
||||
content: map_get($icons, "info-circle");
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-success {
|
||||
background-color: $success;
|
||||
|
||||
.icon i:before {
|
||||
content: map_get($icons, "check");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalshow {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(0, -25%);
|
||||
}
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(0, -25%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes backdropshow {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: $modal-backdrop-opacity;
|
||||
}
|
||||
100% {
|
||||
opacity: $modal-backdrop-opacity;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
.modal.fade & {
|
||||
transform: initial !important;
|
||||
animation: modalshow 0.3s ease-in;
|
||||
}
|
||||
.modal.show & {
|
||||
transform: initial !important;
|
||||
}
|
||||
transform: translate(0, 0);
|
||||
.modal.fade & {
|
||||
transform: initial !important;
|
||||
animation: modalshow 0.3s ease-in;
|
||||
}
|
||||
.modal.show & {
|
||||
transform: initial !important;
|
||||
}
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
&.fade {
|
||||
animation: backdropshow 0.1s ease-in;
|
||||
}
|
||||
opacity: $modal-backdrop-opacity !important;
|
||||
&.fade {
|
||||
animation: backdropshow 0.1s ease-in;
|
||||
}
|
||||
opacity: $modal-backdrop-opacity !important;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@import "bootstrap.scss";
|
||||
@import "../../jslib/angular/src/scss/webfonts.css";
|
||||
@import "../../jslib/angular/src/scss/bwicons/styles/style.scss";
|
||||
@import "bootstrap.scss";
|
||||
@import "pages.scss";
|
||||
@import "misc.scss";
|
||||
@import "plugins.scss";
|
||||
|
||||
37
src/services/api.service.ts
Normal file
37
src/services/api.service.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
68
src/services/auth.service.ts
Normal file
68
src/services/auth.service.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,317 +1,531 @@
|
||||
import * as graph from '@microsoft/microsoft-graph-client';
|
||||
import * as graphType from '@microsoft/microsoft-graph-types';
|
||||
import * as https from 'https';
|
||||
import * as querystring from 'querystring';
|
||||
import * as https from "https";
|
||||
import * as querystring from "querystring";
|
||||
|
||||
import { DirectoryType } from '../enums/directoryType';
|
||||
import * as graph from "@microsoft/microsoft-graph-client";
|
||||
import * as graphType from "@microsoft/microsoft-graph-types";
|
||||
|
||||
import { AzureConfiguration } from '../models/azureConfiguration';
|
||||
import { GroupEntry } from '../models/groupEntry';
|
||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
||||
import { UserEntry } from '../models/userEntry';
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
|
||||
import { BaseDirectoryService } from './baseDirectory.service';
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { DirectoryService } from './directory.service';
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { DirectoryType } from "../enums/directoryType";
|
||||
import { AzureConfiguration } from "../models/azureConfiguration";
|
||||
import { GroupEntry } from "../models/groupEntry";
|
||||
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||
import { UserEntry } from "../models/userEntry";
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LogService } from 'jslib/abstractions/log.service';
|
||||
import { BaseDirectoryService } from "./baseDirectory.service";
|
||||
import { IDirectoryService } from "./directory.service";
|
||||
|
||||
const NextLink = '@odata.nextLink';
|
||||
const DeltaLink = '@odata.deltaLink';
|
||||
const ObjectType = '@odata.type';
|
||||
const AzurePublicIdentityAuhtority = "login.microsoftonline.com";
|
||||
const AzureGovermentIdentityAuhtority = "login.microsoftonline.us";
|
||||
|
||||
export class AzureDirectoryService extends BaseDirectoryService implements DirectoryService {
|
||||
private client: graph.Client;
|
||||
private dirConfig: AzureConfiguration;
|
||||
private syncConfig: SyncConfiguration;
|
||||
private accessToken: string;
|
||||
private accessTokenExpiration: Date;
|
||||
const NextLink = "@odata.nextLink";
|
||||
const DeltaLink = "@odata.deltaLink";
|
||||
const ObjectType = "@odata.type";
|
||||
const UserSelectParams = "?$select=id,mail,userPrincipalName,displayName,accountEnabled";
|
||||
|
||||
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.configurationService.getDirectoryType();
|
||||
if (type !== DirectoryType.AzureActiveDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dirConfig = await this.configurationService.getDirectory<AzureConfiguration>(
|
||||
DirectoryType.AzureActiveDirectory);
|
||||
if (this.dirConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncConfig = await this.configurationService.getSync();
|
||||
if (this.syncConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let users: UserEntry[];
|
||||
if (this.syncConfig.users) {
|
||||
users = await this.getUsers(force, !test);
|
||||
}
|
||||
|
||||
let groups: GroupEntry[];
|
||||
if (this.syncConfig.groups) {
|
||||
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
|
||||
groups = await this.getGroups(this.forceGroup(force, users), !test, setFilter);
|
||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter);
|
||||
}
|
||||
|
||||
return [groups, users];
|
||||
}
|
||||
|
||||
private async getUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
|
||||
const entries: UserEntry[] = [];
|
||||
|
||||
let res: any = null;
|
||||
const token = await this.configurationService.getUserDeltaToken();
|
||||
if (!force && token != null) {
|
||||
try {
|
||||
const deltaReq = this.client.api(token);
|
||||
res = await deltaReq.get();
|
||||
} catch {
|
||||
res = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (res == null) {
|
||||
const userReq = this.client.api('/users/delta');
|
||||
res = await userReq.get();
|
||||
}
|
||||
|
||||
const setFilter = this.createCustomSet(this.syncConfig.userFilter);
|
||||
while (true) {
|
||||
const users: graphType.User[] = res.value;
|
||||
if (users != null) {
|
||||
for (const user of users) {
|
||||
const entry = this.buildUser(user);
|
||||
if (this.filterOutResult(setFilter, entry.email)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.disabled && !entry.deleted &&
|
||||
(entry.email == null || entry.email.indexOf('#') > -1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (res[NextLink] == null) {
|
||||
if (res[DeltaLink] != null && saveDelta) {
|
||||
await this.configurationService.saveUserDeltaToken(res[DeltaLink]);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
const nextReq = this.client.api(res[NextLink]);
|
||||
res = await nextReq.get();
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private buildUser(user: graphType.User): UserEntry {
|
||||
const entry = new UserEntry();
|
||||
entry.referenceId = user.id;
|
||||
entry.externalId = user.id;
|
||||
entry.email = user.mail || user.userPrincipalName;
|
||||
entry.disabled = user.accountEnabled == null ? false : !user.accountEnabled;
|
||||
|
||||
if ((user as any)['@removed'] != null && (user as any)['@removed'].reason === 'changed') {
|
||||
entry.deleted = true;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private async getGroups(force: boolean, saveDelta: boolean,
|
||||
setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
|
||||
const entries: GroupEntry[] = [];
|
||||
const changedGroupIds: string[] = [];
|
||||
const token = await this.configurationService.getGroupDeltaToken();
|
||||
const getFullResults = token == null || force;
|
||||
let res: any = null;
|
||||
let errored = false;
|
||||
|
||||
try {
|
||||
if (!getFullResults) {
|
||||
try {
|
||||
const deltaReq = this.client.api(token);
|
||||
res = await deltaReq.get();
|
||||
} catch {
|
||||
res = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (res == null) {
|
||||
const groupReq = this.client.api('/groups/delta');
|
||||
res = await groupReq.get();
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const groups: graphType.Group[] = res.value;
|
||||
if (groups != null) {
|
||||
for (const group of groups) {
|
||||
if (getFullResults) {
|
||||
if (this.filterOutResult(setFilter, group.displayName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = await this.buildGroup(group);
|
||||
entries.push(entry);
|
||||
} else {
|
||||
changedGroupIds.push(group.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res[NextLink] == null) {
|
||||
if (res[DeltaLink] != null && saveDelta) {
|
||||
await this.configurationService.saveGroupDeltaToken(res[DeltaLink]);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
const nextReq = this.client.api(res[NextLink]);
|
||||
res = await nextReq.get();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
errored = true;
|
||||
}
|
||||
|
||||
if (!errored && (getFullResults || changedGroupIds.length === 0)) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
const allGroupsReq = this.client.api('/groups');
|
||||
res = await allGroupsReq.get();
|
||||
while (true) {
|
||||
const allGroups: graphType.Group[] = res.value;
|
||||
if (allGroups != null) {
|
||||
for (const group of allGroups) {
|
||||
if (this.filterOutResult(setFilter, group.displayName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = await this.buildGroup(group);
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (res[NextLink] == null) {
|
||||
break;
|
||||
} else {
|
||||
const nextReq = this.client.api(res[NextLink]);
|
||||
res = await nextReq.get();
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private async buildGroup(group: graphType.Group): Promise<GroupEntry> {
|
||||
const entry = new GroupEntry();
|
||||
entry.referenceId = group.id;
|
||||
entry.externalId = group.id;
|
||||
entry.name = group.displayName;
|
||||
|
||||
const memReq = this.client.api('/groups/' + group.id + '/members');
|
||||
const memRes = await memReq.get();
|
||||
const members: any = memRes.value;
|
||||
if (members != null) {
|
||||
for (const member of members) {
|
||||
if (member[ObjectType] === '#microsoft.graph.group') {
|
||||
entry.groupMemberReferenceIds.add((member as graphType.Group).id);
|
||||
} else if (member[ObjectType] === '#microsoft.graph.user') {
|
||||
entry.userMemberExternalIds.add((member as graphType.User).id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.client = graph.Client.init({
|
||||
authProvider: (done) => {
|
||||
if (this.dirConfig.applicationId == null || this.dirConfig.key == null ||
|
||||
this.dirConfig.tenant == null) {
|
||||
done(this.i18nService.t('dirConfigIncomplete'), null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.accessTokenIsExpired()) {
|
||||
done(null, this.accessToken);
|
||||
return;
|
||||
}
|
||||
|
||||
this.accessToken = null;
|
||||
this.accessTokenExpiration = null;
|
||||
|
||||
const data = querystring.stringify({
|
||||
client_id: this.dirConfig.applicationId,
|
||||
client_secret: this.dirConfig.key,
|
||||
grant_type: 'client_credentials',
|
||||
scope: 'https://graph.microsoft.com/.default',
|
||||
});
|
||||
|
||||
const req = https.request({
|
||||
host: 'login.microsoftonline.com',
|
||||
path: '/' + this.dirConfig.tenant + '/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': Buffer.byteLength(data),
|
||||
},
|
||||
}, (res) => {
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk: string) => {
|
||||
const d = JSON.parse(chunk);
|
||||
if (res.statusCode === 200 && d.access_token != null) {
|
||||
this.setAccessTokenExpiration(d.access_token, d.expires_in);
|
||||
done(null, d.access_token);
|
||||
} else if (d.error != null && d.error_description != null) {
|
||||
done(d.error + ' (' + res.statusCode + '): ' + d.error_description, null);
|
||||
} else {
|
||||
done('Unknown error (' + res.statusCode + ').', null);
|
||||
}
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
done(err, null);
|
||||
});
|
||||
|
||||
req.write(data);
|
||||
req.end();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private accessTokenIsExpired() {
|
||||
if (this.accessToken == null || this.accessTokenExpiration == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// expired if less than 2 minutes til expiration
|
||||
const now = new Date();
|
||||
return this.accessTokenExpiration.getTime() - now.getTime() < 120000;
|
||||
}
|
||||
|
||||
private setAccessTokenExpiration(accessToken: string, expSeconds: number) {
|
||||
if (accessToken == null || expSeconds == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.accessToken = accessToken;
|
||||
const exp = new Date();
|
||||
exp.setSeconds(exp.getSeconds() + expSeconds);
|
||||
this.accessTokenExpiration = exp;
|
||||
}
|
||||
enum UserSetType {
|
||||
IncludeUser,
|
||||
ExcludeUser,
|
||||
IncludeGroup,
|
||||
ExcludeGroup,
|
||||
}
|
||||
|
||||
export class AzureDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||
private client: graph.Client;
|
||||
private dirConfig: AzureConfiguration;
|
||||
private syncConfig: SyncConfiguration;
|
||||
private accessToken: string;
|
||||
private accessTokenExpiration: Date;
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private stateService: StateService
|
||||
) {
|
||||
super();
|
||||
this.init();
|
||||
}
|
||||
|
||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||
const type = await this.stateService.getDirectoryType();
|
||||
if (type !== DirectoryType.AzureActiveDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dirConfig = await this.stateService.getDirectory<AzureConfiguration>(
|
||||
DirectoryType.AzureActiveDirectory
|
||||
);
|
||||
if (this.dirConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncConfig = await this.stateService.getSync();
|
||||
if (this.syncConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let users: UserEntry[];
|
||||
if (this.syncConfig.users) {
|
||||
users = await this.getCurrentUsers();
|
||||
const deletedUsers = await this.getDeletedUsers(force, !test);
|
||||
users = users.concat(deletedUsers);
|
||||
}
|
||||
|
||||
let groups: GroupEntry[];
|
||||
if (this.syncConfig.groups) {
|
||||
const setFilter = await this.createAadCustomSet(this.syncConfig.groupFilter);
|
||||
groups = await this.getGroups(setFilter);
|
||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
||||
}
|
||||
|
||||
return [groups, users];
|
||||
}
|
||||
|
||||
private async getCurrentUsers(): Promise<UserEntry[]> {
|
||||
let entries: UserEntry[] = [];
|
||||
let users: graphType.User[];
|
||||
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);
|
||||
}
|
||||
if (users != null) {
|
||||
entries = await this.buildUserEntries(users, userIdsToExclude, setFilter);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private async getDeletedUsers(force: boolean, saveDelta: boolean): Promise<UserEntry[]> {
|
||||
const entryIds = new Set<string>();
|
||||
const entries: UserEntry[] = [];
|
||||
|
||||
let res: any = null;
|
||||
const token = await this.stateService.getUserDelta();
|
||||
if (!force && token != null) {
|
||||
try {
|
||||
const deltaReq = this.client.api(token);
|
||||
res = await deltaReq.get();
|
||||
} catch {
|
||||
res = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (res == null) {
|
||||
const userReq = this.client.api("/users/delta" + UserSelectParams);
|
||||
res = await userReq.get();
|
||||
}
|
||||
|
||||
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
const users: graphType.User[] = res.value;
|
||||
if (users != null) {
|
||||
for (const user of users) {
|
||||
if (user.id == null || entryIds.has(user.id)) {
|
||||
continue;
|
||||
}
|
||||
const entry = this.buildUser(user);
|
||||
if (!entry.deleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
setFilter != null &&
|
||||
(setFilter[0] === UserSetType.IncludeUser ||
|
||||
setFilter[0] === UserSetType.ExcludeUser) &&
|
||||
(await this.filterOutUserResult(setFilter, entry))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.push(entry);
|
||||
entryIds.add(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (res[NextLink] == null) {
|
||||
if (res[DeltaLink] != null && saveDelta) {
|
||||
await this.stateService.setUserDelta(res[DeltaLink]);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
const nextReq = this.client.api(res[NextLink]);
|
||||
res = await nextReq.get();
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private async createAadCustomSet(filter: string): Promise<[boolean, Set<string>]> {
|
||||
if (filter == null || filter === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mainParts = filter.split("|");
|
||||
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = mainParts[0].split(":");
|
||||
if (parts.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyword = parts[0].trim().toLowerCase();
|
||||
let exclude = true;
|
||||
if (keyword === "include") {
|
||||
exclude = false;
|
||||
} else if (keyword === "exclude") {
|
||||
exclude = true;
|
||||
} else if (keyword === "excludeadministrativeunit") {
|
||||
exclude = true;
|
||||
} else if (keyword === "includeadministrativeunit") {
|
||||
exclude = false;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const set = new Set<string>();
|
||||
const pieces = parts[1].split(",");
|
||||
if (keyword === "excludeadministrativeunit" || keyword === "includeadministrativeunit") {
|
||||
for (const p of pieces) {
|
||||
const auMembers = await this.client
|
||||
.api(`https://graph.microsoft.com/v1.0/directory/administrativeUnits/${p}/members`)
|
||||
.get();
|
||||
for (const auMember of auMembers.value) {
|
||||
if (auMember["@odata.type"] === "#microsoft.graph.group") {
|
||||
set.add(auMember.displayName.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const p of pieces) {
|
||||
set.add(p.trim().toLowerCase());
|
||||
}
|
||||
}
|
||||
return [exclude, set];
|
||||
}
|
||||
|
||||
private createCustomUserSet(filter: string): [UserSetType, Set<string>] {
|
||||
if (filter == null || filter === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mainParts = filter.split("|");
|
||||
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = mainParts[0].split(":");
|
||||
if (parts.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyword = parts[0].trim().toLowerCase();
|
||||
let userSetType = UserSetType.IncludeUser;
|
||||
if (keyword === "include") {
|
||||
userSetType = UserSetType.IncludeUser;
|
||||
} else if (keyword === "exclude") {
|
||||
userSetType = UserSetType.ExcludeUser;
|
||||
} else if (keyword === "includegroup") {
|
||||
userSetType = UserSetType.IncludeGroup;
|
||||
} else if (keyword === "excludegroup") {
|
||||
userSetType = UserSetType.ExcludeGroup;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const set = new Set<string>();
|
||||
const pieces = parts[1].split(",");
|
||||
for (const p of pieces) {
|
||||
set.add(p.trim().toLowerCase());
|
||||
}
|
||||
|
||||
return [userSetType, set];
|
||||
}
|
||||
|
||||
private async filterOutUserResult(
|
||||
setFilter: [UserSetType, Set<string>],
|
||||
user: UserEntry
|
||||
): Promise<boolean> {
|
||||
if (setFilter == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let userSetTypeExclude = null;
|
||||
if (setFilter[0] === UserSetType.IncludeUser) {
|
||||
userSetTypeExclude = false;
|
||||
} else if (setFilter[0] === UserSetType.ExcludeUser) {
|
||||
userSetTypeExclude = true;
|
||||
}
|
||||
|
||||
if (userSetTypeExclude != null) {
|
||||
return this.filterOutResult([userSetTypeExclude, setFilter[1]], user.email);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private buildUser(user: graphType.User): UserEntry {
|
||||
const entry = new UserEntry();
|
||||
entry.referenceId = user.id;
|
||||
entry.externalId = user.id;
|
||||
entry.email = user.mail;
|
||||
|
||||
if (
|
||||
user.userPrincipalName &&
|
||||
(entry.email == null || entry.email === "" || entry.email.indexOf("onmicrosoft.com") > -1)
|
||||
) {
|
||||
entry.email = user.userPrincipalName;
|
||||
}
|
||||
|
||||
if (entry.email != null) {
|
||||
entry.email = entry.email.trim().toLowerCase();
|
||||
}
|
||||
|
||||
entry.disabled = user.accountEnabled == null ? false : !user.accountEnabled;
|
||||
|
||||
if ((user as any)["@removed"] != null && (user as any)["@removed"].reason === "changed") {
|
||||
entry.deleted = true;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
|
||||
const entryIds = new Set<string>();
|
||||
const entries: GroupEntry[] = [];
|
||||
const groupsReq = this.client.api("/groups");
|
||||
let res = await groupsReq.get();
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
const groups: graphType.Group[] = res.value;
|
||||
if (groups != null) {
|
||||
for (const group of groups) {
|
||||
if (group.id == null || entryIds.has(group.id)) {
|
||||
continue;
|
||||
}
|
||||
if (this.filterOutResult(setFilter, group.displayName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = await this.buildGroup(group);
|
||||
entries.push(entry);
|
||||
entryIds.add(group.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (res[NextLink] == null) {
|
||||
break;
|
||||
} else {
|
||||
const nextReq = this.client.api(res[NextLink]);
|
||||
res = await nextReq.get();
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private async getUsersByResource(usersRequest: graph.GraphRequest) {
|
||||
const users: graphType.User[] = [];
|
||||
let res = await usersRequest.get();
|
||||
res.value.forEach((user: graphType.User) => users.push(user));
|
||||
while (res[NextLink] != null) {
|
||||
const nextReq = this.client.api(res[NextLink]);
|
||||
res = await nextReq.get();
|
||||
res.value.forEach((user: graphType.User) => users.push(user));
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
private async getUsersByGroups(setFilter: [UserSetType, Set<string>]): Promise<graphType.User[]> {
|
||||
const users: graphType.User[] = [];
|
||||
for (const group of setFilter[1]) {
|
||||
const groupUsersReq = this.client.api(
|
||||
`/groups/${group}/transitiveMembers` + UserSelectParams
|
||||
);
|
||||
users.push(...(await this.getUsersByResource(groupUsersReq)));
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
private async buildUserEntries(
|
||||
users: graphType.User[],
|
||||
userIdsToExclude: Set<string>,
|
||||
setFilter: [UserSetType, Set<string>]
|
||||
) {
|
||||
const entryIds = new Set<string>();
|
||||
const entries: UserEntry[] = [];
|
||||
|
||||
for (const user of users) {
|
||||
if (user.id == null || entryIds.has(user.id) || userIdsToExclude.has(user.id)) {
|
||||
continue;
|
||||
}
|
||||
const entry = this.buildUser(user);
|
||||
|
||||
if (
|
||||
setFilter != null &&
|
||||
(setFilter[0] === UserSetType.IncludeUser || setFilter[0] === UserSetType.ExcludeUser) &&
|
||||
(await this.filterOutUserResult(setFilter, entry))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!this.isInvalidUser(entry)) {
|
||||
entries.push(entry);
|
||||
entryIds.add(user.id);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private isInvalidUser(user: UserEntry): boolean {
|
||||
return !user.disabled && !user.deleted && (user.email == null || user.email.indexOf("#") > -1);
|
||||
}
|
||||
|
||||
private async buildGroup(group: graphType.Group): Promise<GroupEntry> {
|
||||
const entry = new GroupEntry();
|
||||
entry.referenceId = group.id;
|
||||
entry.externalId = group.id;
|
||||
entry.name = group.displayName;
|
||||
|
||||
const memReq = this.client.api("/groups/" + group.id + "/members");
|
||||
let memRes = await memReq.get();
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
const members: any = memRes.value;
|
||||
if (members != null) {
|
||||
for (const member of members) {
|
||||
if (member[ObjectType] === "#microsoft.graph.group") {
|
||||
entry.groupMemberReferenceIds.add((member as graphType.Group).id);
|
||||
} else if (member[ObjectType] === "#microsoft.graph.user") {
|
||||
entry.userMemberExternalIds.add((member as graphType.User).id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (memRes[NextLink] == null) {
|
||||
break;
|
||||
} else {
|
||||
const nextMemReq = this.client.api(memRes[NextLink]);
|
||||
memRes = await nextMemReq.get();
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.client = graph.Client.init({
|
||||
authProvider: (done) => {
|
||||
if (
|
||||
this.dirConfig.applicationId == null ||
|
||||
this.dirConfig.key == null ||
|
||||
this.dirConfig.tenant == null
|
||||
) {
|
||||
done(new Error(this.i18nService.t("dirConfigIncomplete")), null);
|
||||
return;
|
||||
}
|
||||
|
||||
const identityAuthority =
|
||||
this.dirConfig.identityAuthority != null
|
||||
? this.dirConfig.identityAuthority
|
||||
: AzurePublicIdentityAuhtority;
|
||||
if (
|
||||
identityAuthority !== AzurePublicIdentityAuhtority &&
|
||||
identityAuthority !== AzureGovermentIdentityAuhtority
|
||||
) {
|
||||
done(new Error(this.i18nService.t("dirConfigIncomplete")), null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.accessTokenIsExpired()) {
|
||||
done(null, this.accessToken);
|
||||
return;
|
||||
}
|
||||
|
||||
this.accessToken = null;
|
||||
this.accessTokenExpiration = null;
|
||||
|
||||
const data = querystring.stringify({
|
||||
client_id: this.dirConfig.applicationId,
|
||||
client_secret: this.dirConfig.key,
|
||||
grant_type: "client_credentials",
|
||||
scope: "https://graph.microsoft.com/.default",
|
||||
});
|
||||
|
||||
const req = https
|
||||
.request(
|
||||
{
|
||||
host: identityAuthority,
|
||||
path: "/" + this.dirConfig.tenant + "/oauth2/v2.0/token",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Content-Length": Buffer.byteLength(data),
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
res.setEncoding("utf8");
|
||||
res.on("data", (chunk: string) => {
|
||||
const d = JSON.parse(chunk);
|
||||
if (res.statusCode === 200 && d.access_token != null) {
|
||||
this.setAccessTokenExpiration(d.access_token, d.expires_in);
|
||||
done(null, d.access_token);
|
||||
} else if (d.error != null && d.error_description != null) {
|
||||
const shortError = d.error_description?.split("\n", 1)[0];
|
||||
const err = new Error(d.error + " (" + res.statusCode + "): " + shortError);
|
||||
// eslint-disable-next-line
|
||||
console.error(d.error_description);
|
||||
done(err, null);
|
||||
} else {
|
||||
const err = new Error("Unknown error (" + res.statusCode + ").");
|
||||
done(err, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
.on("error", (err) => {
|
||||
done(err, null);
|
||||
});
|
||||
|
||||
req.write(data);
|
||||
req.end();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private accessTokenIsExpired() {
|
||||
if (this.accessToken == null || this.accessTokenExpiration == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// expired if less than 2 minutes til expiration
|
||||
const now = new Date();
|
||||
return this.accessTokenExpiration.getTime() - now.getTime() < 120000;
|
||||
}
|
||||
|
||||
private setAccessTokenExpiration(accessToken: string, expSeconds: number) {
|
||||
if (accessToken == null || expSeconds == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.accessToken = accessToken;
|
||||
const exp = new Date();
|
||||
exp.setSeconds(exp.getSeconds() + expSeconds);
|
||||
this.accessTokenExpiration = exp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,94 @@
|
||||
import { GroupEntry } from '../models/groupEntry';
|
||||
import { UserEntry } from '../models/userEntry';
|
||||
import { GroupEntry } from "../models/groupEntry";
|
||||
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||
import { UserEntry } from "../models/userEntry";
|
||||
|
||||
export abstract class BaseDirectoryService {
|
||||
protected createDirectoryQuery(filter: string) {
|
||||
if (filter == null || filter === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mainParts = filter.split('|');
|
||||
if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mainParts[1].trim();
|
||||
protected createDirectoryQuery(filter: string) {
|
||||
if (filter == null || filter === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected createCustomSet(filter: string): [boolean, Set<string>] {
|
||||
if (filter == null || filter === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mainParts = filter.split('|');
|
||||
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = mainParts[0].split(':');
|
||||
if (parts.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyword = parts[0].trim().toLowerCase();
|
||||
let exclude = true;
|
||||
if (keyword === 'include') {
|
||||
exclude = false;
|
||||
} else if (keyword === 'exclude') {
|
||||
exclude = true;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const set = new Set<string>();
|
||||
const pieces = parts[1].split(',');
|
||||
for (const p of pieces) {
|
||||
set.add(p.trim().toLowerCase());
|
||||
}
|
||||
|
||||
return [exclude, set];
|
||||
const mainParts = filter.split("|");
|
||||
if (mainParts.length < 2 || mainParts[1] == null || mainParts[1].trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected filterOutResult(setFilter: [boolean, Set<string>], result: string) {
|
||||
if (setFilter != null) {
|
||||
result = result.trim().toLowerCase();
|
||||
const excluded = setFilter[0];
|
||||
const set = setFilter[1];
|
||||
return mainParts[1].trim();
|
||||
}
|
||||
|
||||
if (excluded && set.has(result)) {
|
||||
return true;
|
||||
} else if (!excluded && !set.has(result)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
protected createCustomSet(filter: string): [boolean, Set<string>] {
|
||||
if (filter == null || filter === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected filterUsersFromGroupsSet(users: UserEntry[], groups: GroupEntry[],
|
||||
setFilter: [boolean, Set<string>]): UserEntry[] {
|
||||
if (setFilter == null || users == null) {
|
||||
return users;
|
||||
}
|
||||
|
||||
return users.filter((u) => {
|
||||
if (u.disabled || u.deleted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return groups.filter((g) => g.userMemberExternalIds.has(u.externalId)).length > 0;
|
||||
});
|
||||
const mainParts = filter.split("|");
|
||||
if (mainParts.length < 1 || mainParts[0] == null || mainParts[0].trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected forceGroup(force: boolean, users: UserEntry[]): boolean {
|
||||
return force || (users != null && users.filter((u) => !u.deleted && !u.disabled).length > 0);
|
||||
const parts = mainParts[0].split(":");
|
||||
if (parts.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyword = parts[0].trim().toLowerCase();
|
||||
let exclude = true;
|
||||
if (keyword === "include") {
|
||||
exclude = false;
|
||||
} else if (keyword === "exclude") {
|
||||
exclude = true;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const set = new Set<string>();
|
||||
const pieces = parts[1].split(",");
|
||||
for (const p of pieces) {
|
||||
set.add(p.trim().toLowerCase());
|
||||
}
|
||||
|
||||
return [exclude, set];
|
||||
}
|
||||
|
||||
protected filterOutResult(setFilter: [boolean, Set<string>], result: string) {
|
||||
if (setFilter != null) {
|
||||
const cleanResult = result != null ? result.trim().toLowerCase() : "--";
|
||||
const excluded = setFilter[0];
|
||||
const set = setFilter[1];
|
||||
|
||||
if (excluded && set.has(cleanResult)) {
|
||||
return true;
|
||||
} else if (!excluded && !set.has(cleanResult)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected filterUsersFromGroupsSet(
|
||||
users: UserEntry[],
|
||||
groups: GroupEntry[],
|
||||
setFilter: [boolean, Set<string>],
|
||||
syncConfig: SyncConfiguration
|
||||
): UserEntry[] {
|
||||
if (setFilter == null || users == null) {
|
||||
return users;
|
||||
}
|
||||
|
||||
return users.filter((u) => {
|
||||
if (u.deleted) {
|
||||
return true;
|
||||
}
|
||||
if (u.disabled && syncConfig.removeDisabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return groups.filter((g) => g.userMemberExternalIds.has(u.externalId)).length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
protected forceGroup(force: boolean, users: UserEntry[]): boolean {
|
||||
return force || (users != null && users.filter((u) => !u.deleted && !u.disabled).length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
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 { SyncConfiguration } from '../models/syncConfiguration';
|
||||
|
||||
const StoredSecurely = '[STORED SECURELY]';
|
||||
const Keys = {
|
||||
ldap: 'ldapPassword',
|
||||
gsuite: 'gsuitePrivateKey',
|
||||
azure: 'azureKey',
|
||||
okta: 'oktaToken',
|
||||
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) { }
|
||||
|
||||
async getDirectory<T>(type: DirectoryType): Promise<T> {
|
||||
const config = await this.storageService.get<T>(Keys.directoryConfigPrefix + type);
|
||||
if (config == null) {
|
||||
return config;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case DirectoryType.Ldap:
|
||||
(config as any).password = await this.secureStorageService.get<string>(Keys.ldap);
|
||||
break;
|
||||
case DirectoryType.AzureActiveDirectory:
|
||||
(config as any).key = await this.secureStorageService.get<string>(Keys.azure);
|
||||
break;
|
||||
case DirectoryType.Okta:
|
||||
(config as any).token = await this.secureStorageService.get<string>(Keys.okta);
|
||||
break;
|
||||
case DirectoryType.GSuite:
|
||||
(config as any).privateKey = await this.secureStorageService.get<string>(Keys.gsuite);
|
||||
break;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
async saveDirectory(type: DirectoryType,
|
||||
config: LdapConfiguration | GSuiteConfiguration | AzureConfiguration | OktaConfiguration): Promise<any> {
|
||||
const savedConfig: any = Object.assign({}, config);
|
||||
switch (type) {
|
||||
case DirectoryType.Ldap:
|
||||
if (savedConfig.password == null) {
|
||||
await this.secureStorageService.remove(Keys.ldap);
|
||||
} else {
|
||||
await this.secureStorageService.save(Keys.ldap, savedConfig.password);
|
||||
savedConfig.password = StoredSecurely;
|
||||
}
|
||||
break;
|
||||
case DirectoryType.AzureActiveDirectory:
|
||||
if (savedConfig.key == null) {
|
||||
await this.secureStorageService.remove(Keys.azure);
|
||||
} else {
|
||||
await this.secureStorageService.save(Keys.azure, savedConfig.key);
|
||||
savedConfig.key = StoredSecurely;
|
||||
}
|
||||
break;
|
||||
case DirectoryType.Okta:
|
||||
if (savedConfig.token == null) {
|
||||
await this.secureStorageService.remove(Keys.okta);
|
||||
} else {
|
||||
await this.secureStorageService.save(Keys.okta, savedConfig.token);
|
||||
savedConfig.token = StoredSecurely;
|
||||
}
|
||||
break;
|
||||
case DirectoryType.GSuite:
|
||||
if (savedConfig.privateKey == null) {
|
||||
await this.secureStorageService.remove(Keys.gsuite);
|
||||
} else {
|
||||
(config as GSuiteConfiguration).privateKey = savedConfig.privateKey =
|
||||
savedConfig.privateKey.replace(/\\n/g, '\n');
|
||||
await this.secureStorageService.save(Keys.gsuite, savedConfig.privateKey);
|
||||
savedConfig.privateKey = StoredSecurely;
|
||||
}
|
||||
break;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GroupEntry } from '../models/groupEntry';
|
||||
import { UserEntry } from '../models/userEntry';
|
||||
import { GroupEntry } from "../models/groupEntry";
|
||||
import { UserEntry } from "../models/userEntry";
|
||||
|
||||
export interface DirectoryService {
|
||||
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
|
||||
export interface IDirectoryService {
|
||||
getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]>;
|
||||
}
|
||||
|
||||
@@ -1,214 +1,263 @@
|
||||
import { JWT } from 'google-auth-library';
|
||||
import {
|
||||
google,
|
||||
GoogleApis,
|
||||
} from 'googleapis';
|
||||
import {
|
||||
Admin,
|
||||
Schema$Group,
|
||||
Schema$User,
|
||||
} from 'googleapis/build/src/apis/admin/directory_v1';
|
||||
import { JWT } from "google-auth-library";
|
||||
import { admin_directory_v1, google } from "googleapis";
|
||||
|
||||
import { DirectoryType } from '../enums/directoryType';
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
|
||||
import { GroupEntry } from '../models/groupEntry';
|
||||
import { GSuiteConfiguration } from '../models/gsuiteConfiguration';
|
||||
import { SyncConfiguration } from '../models/syncConfiguration';
|
||||
import { UserEntry } from '../models/userEntry';
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { DirectoryType } from "../enums/directoryType";
|
||||
import { GroupEntry } from "../models/groupEntry";
|
||||
import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
|
||||
import { SyncConfiguration } from "../models/syncConfiguration";
|
||||
import { UserEntry } from "../models/userEntry";
|
||||
|
||||
import { BaseDirectoryService } from './baseDirectory.service';
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { DirectoryService } 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';
|
||||
export class GSuiteDirectoryService extends BaseDirectoryService implements IDirectoryService {
|
||||
private client: JWT;
|
||||
private service: admin_directory_v1.Admin;
|
||||
private authParams: any;
|
||||
private dirConfig: GSuiteConfiguration;
|
||||
private syncConfig: SyncConfiguration;
|
||||
|
||||
export class GSuiteDirectoryService extends BaseDirectoryService implements DirectoryService {
|
||||
private client: JWT;
|
||||
private service: Admin;
|
||||
private authParams: any;
|
||||
private dirConfig: GSuiteConfiguration;
|
||||
private syncConfig: SyncConfiguration;
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private stateService: StateService
|
||||
) {
|
||||
super();
|
||||
this.service = google.admin("directory_v1");
|
||||
}
|
||||
|
||||
constructor(private configurationService: ConfigurationService, private logService: LogService,
|
||||
private i18nService: I18nService) {
|
||||
super();
|
||||
this.service = google.admin<Admin>('directory_v1');
|
||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||
const type = await this.stateService.getDirectoryType();
|
||||
if (type !== DirectoryType.GSuite) {
|
||||
return;
|
||||
}
|
||||
|
||||
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
|
||||
const type = await this.configurationService.getDirectoryType();
|
||||
if (type !== DirectoryType.GSuite) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dirConfig = await this.configurationService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite);
|
||||
if (this.dirConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncConfig = await this.configurationService.getSync();
|
||||
if (this.syncConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.auth();
|
||||
|
||||
let users: UserEntry[];
|
||||
if (this.syncConfig.users) {
|
||||
users = await this.getUsers();
|
||||
}
|
||||
|
||||
let groups: GroupEntry[];
|
||||
if (this.syncConfig.groups) {
|
||||
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
|
||||
groups = await this.getGroups(setFilter);
|
||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter);
|
||||
}
|
||||
|
||||
return [groups, users];
|
||||
this.dirConfig = await this.stateService.getDirectory<GSuiteConfiguration>(
|
||||
DirectoryType.GSuite
|
||||
);
|
||||
if (this.dirConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
private async getUsers(): Promise<UserEntry[]> {
|
||||
const entries: UserEntry[] = [];
|
||||
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
|
||||
|
||||
this.logService.info('Querying users.');
|
||||
let p = Object.assign({ query: query }, this.authParams);
|
||||
const res = await this.service.users.list(p);
|
||||
if (res.status !== 200) {
|
||||
throw new Error('User list API failed: ' + res.statusText);
|
||||
}
|
||||
|
||||
const filter = this.createCustomSet(this.syncConfig.userFilter);
|
||||
if (res.data.users != null) {
|
||||
for (const user of res.data.users) {
|
||||
if (this.filterOutResult(filter, user.primaryEmail)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = this.buildUser(user, false);
|
||||
if (entry != null) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logService.info('Querying deleted users.');
|
||||
p = Object.assign({ showDeleted: true, query: query }, this.authParams);
|
||||
const delRes = await this.service.users.list(p);
|
||||
if (delRes.status !== 200) {
|
||||
throw new Error('Deleted user list API failed: ' + delRes.statusText);
|
||||
}
|
||||
|
||||
if (delRes.data.users != null) {
|
||||
for (const user of delRes.data.users) {
|
||||
if (this.filterOutResult(filter, user.primaryEmail)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = this.buildUser(user, true);
|
||||
if (entry != null) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
this.syncConfig = await this.stateService.getSync();
|
||||
if (this.syncConfig == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
private buildUser(user: Schema$User, deleted: boolean) {
|
||||
if ((user.emails == null || user.emails === '') && !deleted) {
|
||||
return null;
|
||||
}
|
||||
await this.auth();
|
||||
|
||||
const entry = new UserEntry();
|
||||
entry.referenceId = user.id;
|
||||
entry.externalId = user.id;
|
||||
entry.email = user.primaryEmail;
|
||||
entry.disabled = user.suspended || false;
|
||||
entry.deleted = deleted;
|
||||
let users: UserEntry[] = [];
|
||||
if (this.syncConfig.users) {
|
||||
users = await this.getUsers();
|
||||
}
|
||||
|
||||
let groups: GroupEntry[];
|
||||
if (this.syncConfig.groups) {
|
||||
const setFilter = this.createCustomSet(this.syncConfig.groupFilter);
|
||||
groups = await this.getGroups(setFilter, users);
|
||||
users = this.filterUsersFromGroupsSet(users, groups, setFilter, this.syncConfig);
|
||||
}
|
||||
|
||||
return [groups, users];
|
||||
}
|
||||
|
||||
private async getUsers(): Promise<UserEntry[]> {
|
||||
const entries: UserEntry[] = [];
|
||||
const query = this.createDirectoryQuery(this.syncConfig.userFilter);
|
||||
let nextPageToken: string = null;
|
||||
|
||||
const filter = this.createCustomSet(this.syncConfig.userFilter);
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
this.logService.info("Querying users - nextPageToken:" + nextPageToken);
|
||||
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
|
||||
const res = await this.service.users.list(p);
|
||||
if (res.status !== 200) {
|
||||
throw new Error("User list API failed: " + res.statusText);
|
||||
}
|
||||
|
||||
nextPageToken = res.data.nextPageToken;
|
||||
if (res.data.users != null) {
|
||||
for (const user of res.data.users) {
|
||||
if (this.filterOutResult(filter, user.primaryEmail)) {
|
||||
continue;
|
||||
}
|
||||
const entry = this.buildUser(user, false);
|
||||
if (entry != null) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nextPageToken == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
nextPageToken = null;
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
this.logService.info("Querying deleted users - nextPageToken:" + nextPageToken);
|
||||
const p = Object.assign(
|
||||
{ showDeleted: true, query: query, pageToken: nextPageToken },
|
||||
this.authParams
|
||||
);
|
||||
const delRes = await this.service.users.list(p);
|
||||
if (delRes.status !== 200) {
|
||||
throw new Error("Deleted user list API failed: " + delRes.statusText);
|
||||
}
|
||||
|
||||
nextPageToken = delRes.data.nextPageToken;
|
||||
if (delRes.data.users != null) {
|
||||
for (const user of delRes.data.users) {
|
||||
if (this.filterOutResult(filter, user.primaryEmail)) {
|
||||
continue;
|
||||
}
|
||||
const entry = this.buildUser(user, true);
|
||||
if (entry != null) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nextPageToken == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private buildUser(user: admin_directory_v1.Schema$User, deleted: boolean) {
|
||||
if ((user.emails == null || user.emails === "") && !deleted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry = new UserEntry();
|
||||
entry.referenceId = user.id;
|
||||
entry.externalId = user.id;
|
||||
entry.email = user.primaryEmail != null ? user.primaryEmail.trim().toLowerCase() : null;
|
||||
entry.disabled = user.suspended || false;
|
||||
entry.deleted = deleted;
|
||||
return entry;
|
||||
}
|
||||
|
||||
private async getGroups(
|
||||
setFilter: [boolean, Set<string>],
|
||||
users: UserEntry[]
|
||||
): Promise<GroupEntry[]> {
|
||||
const entries: GroupEntry[] = [];
|
||||
let nextPageToken: string = null;
|
||||
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
this.logService.info("Querying groups - nextPageToken:" + nextPageToken);
|
||||
const p = Object.assign({ pageToken: nextPageToken }, this.authParams);
|
||||
const res = await this.service.groups.list(p);
|
||||
if (res.status !== 200) {
|
||||
throw new Error("Group list API failed: " + res.statusText);
|
||||
}
|
||||
|
||||
nextPageToken = res.data.nextPageToken;
|
||||
if (res.data.groups != null) {
|
||||
for (const group of res.data.groups) {
|
||||
if (!this.filterOutResult(setFilter, group.name)) {
|
||||
const entry = await this.buildGroup(group, users);
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nextPageToken == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private async buildGroup(group: admin_directory_v1.Schema$Group, users: UserEntry[]) {
|
||||
let nextPageToken: string = null;
|
||||
|
||||
const entry = new GroupEntry();
|
||||
entry.referenceId = group.id;
|
||||
entry.externalId = group.id;
|
||||
entry.name = group.name;
|
||||
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
|
||||
const memRes = await this.service.members.list(p);
|
||||
if (memRes.status !== 200) {
|
||||
this.logService.warning("Group member list API failed: " + memRes.statusText);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
private async getGroups(setFilter: [boolean, Set<string>]): Promise<GroupEntry[]> {
|
||||
const entries: GroupEntry[] = [];
|
||||
|
||||
this.logService.info('Querying groups.');
|
||||
const res = await this.service.groups.list(this.authParams);
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Group list API failed: ' + res.statusText);
|
||||
}
|
||||
if (res.data.groups != null) {
|
||||
for (const group of res.data.groups) {
|
||||
if (!this.filterOutResult(setFilter, group.name)) {
|
||||
const entry = await this.buildGroup(group);
|
||||
entries.push(entry);
|
||||
}
|
||||
nextPageToken = memRes.data.nextPageToken;
|
||||
if (memRes.data.members != null) {
|
||||
for (const member of memRes.data.members) {
|
||||
if (member.type == null) {
|
||||
continue;
|
||||
}
|
||||
const type = member.type.toLowerCase();
|
||||
if (type === "user") {
|
||||
if (member.status == null || member.status.toLowerCase() !== "active") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private async buildGroup(group: Schema$Group) {
|
||||
const entry = new GroupEntry();
|
||||
entry.referenceId = group.id;
|
||||
entry.externalId = group.id;
|
||||
entry.name = group.name;
|
||||
|
||||
const p = Object.assign({ groupKey: group.id }, this.authParams);
|
||||
const memRes = await this.service.members.list(p);
|
||||
if (memRes.status !== 200) {
|
||||
this.logService.warning('Group member list API failed: ' + memRes.statusText);
|
||||
return entry;
|
||||
}
|
||||
|
||||
if (memRes.data.members != null) {
|
||||
for (const member of memRes.data.members) {
|
||||
if (member.role.toLowerCase() !== 'member') {
|
||||
continue;
|
||||
}
|
||||
if (member.status.toLowerCase() !== 'active') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (member.type.toLowerCase() === 'user') {
|
||||
entry.userMemberExternalIds.add(member.id);
|
||||
} else if (member.type.toLowerCase() === 'group') {
|
||||
entry.groupMemberReferenceIds.add(member.id);
|
||||
}
|
||||
entry.userMemberExternalIds.add(member.id);
|
||||
} else if (type === "group") {
|
||||
entry.groupMemberReferenceIds.add(member.id);
|
||||
} else if (type === "customer") {
|
||||
for (const user of users) {
|
||||
entry.userMemberExternalIds.add(user.externalId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
if (nextPageToken == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async auth() {
|
||||
if (this.dirConfig.clientEmail == null || this.dirConfig.privateKey == null ||
|
||||
this.dirConfig.adminUser == null || this.dirConfig.domain == null) {
|
||||
throw new Error(this.i18nService.t('dirConfigIncomplete'));
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
this.client = new google.auth.JWT({
|
||||
email: this.dirConfig.clientEmail,
|
||||
key: this.dirConfig.privateKey,
|
||||
subject: this.dirConfig.adminUser,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/admin.directory.user.readonly',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.readonly',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member.readonly',
|
||||
],
|
||||
});
|
||||
|
||||
await this.client.authorize();
|
||||
|
||||
this.authParams = {
|
||||
auth: this.client,
|
||||
domain: this.dirConfig.domain,
|
||||
};
|
||||
if (this.dirConfig.customer != null) {
|
||||
this.authParams.customer = this.dirConfig.customer;
|
||||
}
|
||||
private async auth() {
|
||||
if (
|
||||
this.dirConfig.clientEmail == null ||
|
||||
this.dirConfig.privateKey == null ||
|
||||
this.dirConfig.adminUser == null ||
|
||||
this.dirConfig.domain == null
|
||||
) {
|
||||
throw new Error(this.i18nService.t("dirConfigIncomplete"));
|
||||
}
|
||||
|
||||
this.client = new google.auth.JWT({
|
||||
email: this.dirConfig.clientEmail,
|
||||
key: this.dirConfig.privateKey != null ? this.dirConfig.privateKey.trimLeft() : null,
|
||||
subject: this.dirConfig.adminUser,
|
||||
scopes: [
|
||||
"https://www.googleapis.com/auth/admin.directory.user.readonly",
|
||||
"https://www.googleapis.com/auth/admin.directory.group.readonly",
|
||||
"https://www.googleapis.com/auth/admin.directory.group.member.readonly",
|
||||
],
|
||||
});
|
||||
|
||||
await this.client.authorize();
|
||||
|
||||
this.authParams = {
|
||||
auth: this.client,
|
||||
};
|
||||
if (this.dirConfig.domain != null && this.dirConfig.domain.trim() !== "") {
|
||||
this.authParams.domain = this.dirConfig.domain;
|
||||
}
|
||||
if (this.dirConfig.customer != null && this.dirConfig.customer.trim() !== "") {
|
||||
this.authParams.customer = this.dirConfig.customer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user