Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72639bd6a3 | ||
|
|
31f0044c0f | ||
|
|
d0924f7e6d | ||
|
|
d2616acf36 | ||
|
|
dfd116bd56 | ||
|
|
830acef847 | ||
|
|
ee7c916494 | ||
|
|
46d477ea5b | ||
|
|
177b4af695 | ||
|
|
d2b8805605 | ||
|
|
39dedc68ec | ||
|
|
bd67756cda | ||
|
|
dd37fa4270 | ||
|
|
3c5c048152 | ||
|
|
dd6e4ef2d0 | ||
|
|
bce79f3488 | ||
|
|
74793a2399 | ||
|
|
6b120b053a | ||
|
|
1d0d62337e | ||
|
|
df8bfc42bb | ||
|
|
ff087738fc | ||
|
|
27567c88c9 | ||
|
|
9077a747d6 | ||
|
|
b359d9f525 | ||
|
|
ad7bd2e52d | ||
|
|
36aed9588b | ||
|
|
f8c81a29c4 | ||
|
|
c91771e152 | ||
|
|
3b7873302b | ||
|
|
d79bd54f0b | ||
|
|
51dc9a0e49 | ||
|
|
b49e55d400 | ||
|
|
33cb7e0a50 | ||
|
|
9edc90306e | ||
|
|
0a75e05b28 | ||
|
|
4a179ac3e3 | ||
|
|
42e32f9568 | ||
|
|
0223755b2a | ||
|
|
30a5f2a701 | ||
|
|
d2d351c9f7 | ||
|
|
e1696e9a4d | ||
|
|
25f015b179 | ||
|
|
9e263379a7 | ||
|
|
4c7ea161cd | ||
|
|
dc026c44a1 | ||
|
|
cb8b21c10c | ||
|
|
47c1beadcd | ||
|
|
8f7eb2abea | ||
|
|
c2b0f321de | ||
|
|
ba7b8b8824 | ||
|
|
9e612cf83d | ||
|
|
2154251c30 | ||
|
|
2d38266609 | ||
|
|
d414df4ca4 | ||
|
|
eab93cf6c5 | ||
|
|
3304def44a | ||
|
|
a4bf02b21b | ||
|
|
e0982da744 | ||
|
|
8abed1a809 | ||
|
|
a9a6518b23 | ||
|
|
1bafbe1ad2 | ||
|
|
a26074a3b8 | ||
|
|
90fd2f1447 | ||
|
|
e59690e703 | ||
|
|
bf06e9f654 | ||
|
|
84359d8e79 | ||
|
|
7801429530 | ||
|
|
dc9376a460 | ||
|
|
1f4076d8d4 | ||
|
|
f202d0f533 | ||
|
|
4344dcbb36 | ||
|
|
87d6bbae2a | ||
|
|
7a42bf456e | ||
|
|
a51f1eee8e | ||
|
|
5b72a916e4 | ||
|
|
42013a919f | ||
|
|
3918f82e14 | ||
|
|
6e79a2a133 | ||
|
|
088deaed3a | ||
|
|
7504e7710c | ||
|
|
bb7352ca54 | ||
|
|
b42ecbff36 | ||
|
|
5fd9f0216a | ||
|
|
942ea99459 | ||
|
|
990e2d6b9d | ||
|
|
f776ba0617 | ||
|
|
fb57965457 | ||
|
|
b99fce7d55 | ||
|
|
0a2d743ea2 | ||
|
|
fbd613084d | ||
|
|
4cd56f8ae5 | ||
|
|
c0d41c0a62 | ||
|
|
389c635030 | ||
|
|
d93a0c170c | ||
|
|
aa18f599ce | ||
|
|
aa7eb2e844 | ||
|
|
8e31adb9db | ||
|
|
f0feb9e563 | ||
|
|
44624e3820 | ||
|
|
225c94b853 | ||
|
|
bd1b8542eb | ||
|
|
c92e93ab1c | ||
|
|
1358727832 | ||
|
|
9bc84a2c59 | ||
|
|
7f00a83b19 | ||
|
|
19426cfd51 | ||
|
|
30c05eb834 | ||
|
|
f1d334b848 | ||
|
|
961f497b6b | ||
|
|
89f33f3714 | ||
|
|
4bad52f43a | ||
|
|
fd6cd3ddf7 | ||
|
|
adf7f8603b | ||
|
|
2a2e4350ee | ||
|
|
f563a4ef45 | ||
|
|
37b6ef6d3f | ||
|
|
96ff0ddf27 | ||
|
|
746862a897 | ||
|
|
5d6e65f206 | ||
|
|
6f9a464302 | ||
|
|
abf7866292 | ||
|
|
10c98c66e7 | ||
|
|
c1f7fe2ddd | ||
|
|
7f4586ed9e | ||
|
|
0b6049c8b0 | ||
|
|
2d347171a9 | ||
|
|
a085d4d4f7 | ||
|
|
7e2f3e3df3 | ||
|
|
3899daea7c | ||
|
|
3d1ee732b4 | ||
|
|
899d7beee0 | ||
|
|
4c7f3fad75 | ||
|
|
b8eb2c3c9b | ||
|
|
f5d612c8b5 | ||
|
|
4020f2d4e2 | ||
|
|
5794cfc7b2 | ||
|
|
1284a16978 | ||
|
|
f255a470cd | ||
|
|
f738e71657 | ||
|
|
beeee851ac | ||
|
|
eb365f0d39 | ||
|
|
57e614e8db | ||
|
|
738fa1f15f | ||
|
|
4c959ab297 | ||
|
|
e685295d04 | ||
|
|
204bd10e04 | ||
|
|
5a302b0bcc | ||
|
|
daa23f66c4 | ||
|
|
b06c396086 | ||
|
|
5555603a5c | ||
|
|
7c3b6d6d9e | ||
|
|
ce87d7728c | ||
|
|
e4da851172 | ||
|
|
e3433ec1f7 | ||
|
|
96f30d1b31 | ||
|
|
f4a48467e3 | ||
|
|
bada405511 | ||
|
|
96d7c055b0 | ||
|
|
a747614a74 | ||
|
|
84856407ee | ||
|
|
9bfd624713 | ||
|
|
b2a6e4d47f | ||
|
|
308240be89 | ||
|
|
c12edc7f73 | ||
|
|
44bd5ee1c6 | ||
|
|
8f3cccce32 | ||
|
|
1ef7d9b5a7 | ||
|
|
c445f95aa4 | ||
|
|
96db738f4b | ||
|
|
6181df4c69 | ||
|
|
376787fda1 | ||
|
|
80ffa856bd | ||
|
|
eb9fe573b8 | ||
|
|
e222d5b909 | ||
|
|
1c774aa783 | ||
|
|
04b51da60f | ||
|
|
28ddec6898 | ||
|
|
d987f844de | ||
|
|
b0d26fe1e6 | ||
|
|
e96131a933 | ||
|
|
3d58745bf1 | ||
|
|
9560148253 | ||
|
|
ec00efb656 | ||
|
|
ba34307918 | ||
|
|
28d8afae2e | ||
|
|
2e507be454 | ||
|
|
dbf3617224 | ||
|
|
200a7944e6 | ||
|
|
8dafc2b9a0 | ||
|
|
b7559c1f4e | ||
|
|
dd8e1864db | ||
|
|
d85a84980f | ||
|
|
4295966b7f | ||
|
|
a87c079ade | ||
|
|
b3604e3e40 | ||
|
|
6be8130329 | ||
|
|
ca3972c04f | ||
|
|
4d98729cde | ||
|
|
185af6f00e | ||
|
|
c5353d6e5e | ||
|
|
20a05dc96a | ||
|
|
30028b5ba0 | ||
|
|
3bafb708ea | ||
|
|
24d86a230a | ||
|
|
620f822504 | ||
|
|
93a57f26ee | ||
|
|
40ec4493ea | ||
|
|
de83d59bde | ||
|
|
a937c5e8de | ||
|
|
b820bb19d7 | ||
|
|
36db2d3f1d | ||
|
|
5786b86521 | ||
|
|
c6a5f93b44 | ||
|
|
8814e2eacb | ||
|
|
9b1e4a8291 | ||
|
|
01c2468f86 | ||
|
|
af705718ff | ||
|
|
e22ef12529 | ||
|
|
62510d9243 | ||
|
|
0fd25a6bb4 | ||
|
|
a268a7b75e | ||
|
|
3dded0a7bc | ||
|
|
ab3140be0a | ||
|
|
0fcd58c080 | ||
|
|
52f0d9f9e8 | ||
|
|
6585fa8e41 | ||
|
|
bef9b3b276 | ||
|
|
af411b854a | ||
|
|
a697dba5b9 | ||
|
|
1203081ec7 | ||
|
|
2db63ae2f8 | ||
|
|
9dd1617b57 | ||
|
|
834058ff45 | ||
|
|
0f41a0a576 | ||
|
|
1025cbfb87 | ||
|
|
ca3c1c7623 | ||
|
|
69d127a89e | ||
|
|
b4d9cd5bb7 | ||
|
|
4b1327db24 | ||
|
|
2f2ceed24e | ||
|
|
de0f20809d | ||
|
|
b1f5857cac | ||
|
|
ddbd1ca163 | ||
|
|
777e40fff8 | ||
|
|
68460331f5 | ||
|
|
32e9fe8b6a | ||
|
|
b3a2460b3f | ||
|
|
3260bf1b2a | ||
|
|
e69c6456d1 | ||
|
|
f8b3792e62 | ||
|
|
01c34c0a79 | ||
|
|
825751239f | ||
|
|
2465739060 | ||
|
|
e6834d2033 | ||
|
|
924100df4c | ||
|
|
1ac7d1e08b | ||
|
|
7becccd628 | ||
|
|
dff5cac4fe | ||
|
|
ecd0c1de20 | ||
|
|
878cb17b62 | ||
|
|
706981c231 | ||
|
|
d212cf7c40 | ||
|
|
19b6e5154f | ||
|
|
89263918ea | ||
|
|
e8fc6d163b | ||
|
|
34a670f8d7 | ||
|
|
6a46e2dd57 | ||
|
|
5e0fa7c744 | ||
|
|
474ce17a13 | ||
|
|
f00eed8633 | ||
|
|
42bcefa068 | ||
|
|
29bde998ed | ||
|
|
e29a83ec25 | ||
|
|
ddfcbde7d4 | ||
|
|
42e31d3ece | ||
|
|
cd8480bc67 | ||
|
|
06dc05be08 | ||
|
|
d3fda81af3 | ||
|
|
f58d345b63 | ||
|
|
f35382a4e0 | ||
|
|
8087430dd2 | ||
|
|
2a5f1f3668 | ||
|
|
a889cb30e0 | ||
|
|
e411c0ab7b | ||
|
|
a136371c1f | ||
|
|
83fa213854 | ||
|
|
750cc1ecae | ||
|
|
e25a854d24 | ||
|
|
3b32195271 | ||
|
|
f9411794c4 | ||
|
|
c5fdbfd62d | ||
|
|
f12ae90aba | ||
|
|
20c4bbcee2 | ||
|
|
f3232f34c2 | ||
|
|
8fedb23399 | ||
|
|
4ca519ce62 | ||
|
|
6d4768272a | ||
|
|
e8cc22879f | ||
|
|
b287683a61 | ||
|
|
ea43ee99d6 | ||
|
|
48db68e7ac | ||
|
|
7807b7a280 | ||
|
|
ad0e223742 | ||
|
|
c9a74f2800 | ||
|
|
570ed61edb | ||
|
|
835b302c19 | ||
|
|
a0fad9874a | ||
|
|
cb895024e0 | ||
|
|
2e94ebdb64 | ||
|
|
ae1e6d2748 | ||
|
|
c72d4f35a0 | ||
|
|
9742d824c4 | ||
|
|
639aa47e19 | ||
|
|
5fa0d11aa9 | ||
|
|
fed32aa57b | ||
|
|
1e2134bd7e | ||
|
|
064375d457 | ||
|
|
deff38de56 | ||
|
|
15950d3a09 | ||
|
|
c4539b8e30 | ||
|
|
904864b14e | ||
|
|
390d179917 | ||
|
|
3b6a421af2 | ||
|
|
cc8d4fe952 | ||
|
|
dfa62dd04b | ||
|
|
520020f2c6 | ||
|
|
31f1ed8dd4 | ||
|
|
5abd4f1f4f | ||
|
|
862ce645cf | ||
|
|
9eccbb91db | ||
|
|
343e1d2193 | ||
|
|
9a11925216 | ||
|
|
f7f1041daa | ||
|
|
1cbf3b227e | ||
|
|
faf4ecfac6 | ||
|
|
ca4aad0a2b | ||
|
|
ed569306bb | ||
|
|
65abd7a4e3 | ||
|
|
422cff4547 | ||
|
|
7695135302 | ||
|
|
5be4868e9a | ||
|
|
fce97fd57a | ||
|
|
af8655ec45 | ||
|
|
daed714b98 | ||
|
|
584e62cff3 | ||
|
|
6d2f1dcbbf | ||
|
|
5e7b74a7ed | ||
|
|
6de20b03ef | ||
|
|
2869020e65 | ||
|
|
40c8ceeca6 | ||
|
|
0623d4ad47 | ||
|
|
0b9c235dd2 | ||
|
|
e16b4e68c8 | ||
|
|
078e36f9b8 | ||
|
|
8874d0be86 | ||
|
|
b59afd3455 | ||
|
|
85a2684532 | ||
|
|
f9a7864b07 | ||
|
|
371301b4ba | ||
|
|
9c10d79661 | ||
|
|
018d879a0a | ||
|
|
b64f1876dc | ||
|
|
055607b3dd | ||
|
|
8a7fa26cb5 | ||
|
|
9cac16fe6a | ||
|
|
61297dba02 | ||
|
|
2f8a53e3ea | ||
|
|
96b9a20844 | ||
|
|
443f230251 | ||
|
|
86ee79e6bf | ||
|
|
b24e5a2c22 | ||
|
|
8c6b1aa69a | ||
|
|
36c2aee271 | ||
|
|
b049635d28 | ||
|
|
42dd029611 | ||
|
|
e02c50aef8 | ||
|
|
d9cf112694 | ||
|
|
a4456f50cd | ||
|
|
a358061f22 | ||
|
|
f155e8ad44 | ||
|
|
c930bb235b | ||
|
|
543cf4697f | ||
|
|
66e9e971a1 | ||
|
|
a961b5e4be | ||
|
|
f8796b362e | ||
|
|
415f638028 | ||
|
|
4523615829 | ||
|
|
61214af01a | ||
|
|
2f0bf5dc17 | ||
|
|
6e71712092 | ||
|
|
7e56e1cccf | ||
|
|
1e3f533d83 | ||
|
|
57568f9a8c | ||
|
|
7c1814118d | ||
|
|
ca50a963fd | ||
|
|
6e188c6996 | ||
|
|
0095c5c666 | ||
|
|
bdd7f303c4 | ||
|
|
f0383522d8 | ||
|
|
a32cc13171 | ||
|
|
dbda8c5674 | ||
|
|
186873d9d4 | ||
|
|
37482c0b36 | ||
|
|
ccdc5a03ac | ||
|
|
2ae24c4627 | ||
|
|
b9e9fd21ee | ||
|
|
6eb83b2a25 | ||
|
|
70aceb1699 | ||
|
|
5e208c4115 | ||
|
|
390f051e43 | ||
|
|
ab0f1fd549 | ||
|
|
788df7b16d | ||
|
|
01a2d8e9a6 | ||
|
|
e842eb001d | ||
|
|
f195e5dc9d | ||
|
|
7f7f0f1859 | ||
|
|
b494a703eb | ||
|
|
0f52b86364 | ||
|
|
1b31188d5e | ||
|
|
30e7268297 | ||
|
|
6956b30fd6 | ||
|
|
98d19ef591 | ||
|
|
f0ffcea424 | ||
|
|
6454fb7061 | ||
|
|
c0d7af7899 | ||
|
|
2424395415 |
No files matched your search
@@ -13,7 +13,10 @@ project {
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/coverage/**",
|
||||
"**/pnpm-lock.yaml",
|
||||
"**/pnpm-workspace.yaml",
|
||||
"ui/desktop/electron-app/out/**",
|
||||
"ui/desktop/electron-app/ember-dist/**",
|
||||
"e2e-tests/api-client/**"
|
||||
]
|
||||
}
|
||||
@@ -11,21 +11,57 @@ assignees: ''
|
||||
|
||||
For questions, the best place to get answers is on our [discussion forum](https://discuss.hashicorp.com/c/boundary), as they will get more visibility from experienced users than the issue tracker.
|
||||
|
||||
Please note: We take Boundary's security and our users' trust very seriously. If you believe you have found a security issue in Boundary, please responsibly disclose by contacting us at security@hashicorp.com. Our PGP key is available at [our security page](https://www.hashicorp.com/security/).
|
||||
Please note: We take Boundary's security and our users' trust very seriously. If you believe you have found a security issue in Boundary, please responsibly disclose this by contacting us at security@hashicorp.com. Our PGP key is available on [our security page](https://www.hashicorp.com/security/).
|
||||
|
||||
Logging is available on Boundary Desktop Client version >= 2.1.0
|
||||
|
||||
Logs are found at:
|
||||
|
||||
Linux: ~/.config/Boundary/logs/desktop-client.log
|
||||
MacOS: ~/Library/Logs/Boundary/desktop-client.log
|
||||
Windows: $env:USERPROFILE\AppData\Roaming\Boundary\logs\desktop-client.log
|
||||
|
||||
If requests are taking longer than expected, please change the logging level to DEBUG. This will log the cache daemon request times as well.
|
||||
|
||||
-->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
## :bug: Bug Report
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Run `boundary authenticate ...`
|
||||
2. Run `boundary connect ...`
|
||||
3. See error
|
||||
### **Description**
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
_A clear and concise description of the bug._
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
### **Steps to Reproduce**
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. See the error
|
||||
|
||||
## **Expected behavior**
|
||||
|
||||
_A clear description of what you expected to happen._
|
||||
|
||||
## **Actual behavior**
|
||||
|
||||
_A clear description of what actually happened._
|
||||
|
||||
## **Screenshots (if applicable)**
|
||||
|
||||
_Add screenshots or screen recordings to help explain the issue._
|
||||
|
||||
## **Environment**
|
||||
|
||||
- **OS and Version:** (e.g., Windows 10 (22H2), macOS (14.7.2))
|
||||
- **Architecture:** (e.g., arm64, x86_64)
|
||||
- **Browser/Version:** (if applicable)
|
||||
- **Desktop Client Version:** (if applicable)
|
||||
- **Controller/worker versions:** (if applicable)
|
||||
|
||||
## **Logs & Console Output (if applicable)**
|
||||
|
||||
_Paste any relevant error logs or browser console output here._
|
||||
|
||||
## **Additional Context**
|
||||
|
||||
_Any other details that might be relevant._
|
||||
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
@@ -12,9 +12,22 @@
|
||||
## Checklist
|
||||
<!-- strikethrough the checklist item that is not relevant to your change -->
|
||||
|
||||
<!-- If you are merging a long lived branch, major feature, or UI altering changes,
|
||||
please add the a11y-tests label. Other cases, like a dependency change or
|
||||
translation update likely does not need an a11y audit -->
|
||||
|
||||
- [ ] I have added before and after screenshots for UI changes
|
||||
- [ ] I have added JSON response output for API changes
|
||||
- [ ] I have added steps to reproduce and test for bug fixes in the description
|
||||
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] I have added `a11y-tests` label to run a11y audit tests if needed
|
||||
|
||||
## PCI review checklist
|
||||
|
||||
<!-- heimdall_github_prtemplate:grc-pci_dss-2024-01-05 -->
|
||||
- [ ] I have documented a clear reason for, and description of, the change I am making.
|
||||
- [ ] If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.
|
||||
- [ ] If applicable, I've documented the impact of any changes to security controls.
|
||||
Examples of changes to security controls include using new access control methods, adding or removing logging pipelines, etc.
|
||||
@@ -27,7 +27,6 @@ echo "Trying to merge the latest oss commit - ${latest_oss_commit} from oss/${BR
|
||||
if ! errors=$(git merge -m "Merge Boundary UI OSS branch '${BRANCH}' at commit ${latest_oss_commit}" "${latest_oss_commit}"); then
|
||||
# Here we can try to auto resolve conflicts
|
||||
# This logic will vary from project to project. The below is just an example.
|
||||
# We'll try to resolve package.json and yarn.lock conflicts by taking the OSS changes
|
||||
conflicts=$(git diff --name-only --diff-filter=U)
|
||||
printf "Found conflicts:\n${conflicts}\n"
|
||||
# Resolve package.json conflicts by taking oss
|
||||
@@ -36,11 +35,12 @@ if ! errors=$(git merge -m "Merge Boundary UI OSS branch '${BRANCH}' at commit $
|
||||
git checkout --theirs package.json
|
||||
git add package.json
|
||||
fi
|
||||
# Resolve yarn.lock conflicts by taking oss
|
||||
if echo "$conflicts" | grep -q yarn.lock; then
|
||||
echo "Checking out oss yarn.lock..."
|
||||
git checkout --theirs yarn.lock
|
||||
git add yarn.lock
|
||||
# Resolve pnpm-lock.yaml conflicts by re-running pnpm install:
|
||||
# https://pnpm.io/git#merge-conflicts
|
||||
if echo "$conflicts" | grep -q pnpm-lock.yaml; then
|
||||
echo "Re-running pnpm install to fix conflicts by regenerating pnpm-lock.yaml..."
|
||||
pnpm install
|
||||
git add pnpm-lock.yaml
|
||||
fi
|
||||
|
||||
# If we can commit all the changes without conflict, we have resolved everything automatically
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
name: A11y Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, opened, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
a11y-test:
|
||||
name: Run A11y Tests
|
||||
if: contains(github.event.pull_request.labels.*.name, 'a11y-tests')
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
- uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1.7.3
|
||||
with:
|
||||
install-dependencies: true
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
run_install: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install
|
||||
- name: Run a11y tests
|
||||
env:
|
||||
# this will generate the test reports but causes tests to pass even when there a11y failures
|
||||
ENABLE_A11Y_MIDDLEWARE_REPORTER: true
|
||||
run: pnpm test-a11y
|
||||
- name: Consolidate ember a11y reports
|
||||
run: |
|
||||
mkdir ember-a11y-reports
|
||||
jq -s 'map(.[])' ./ui/admin/ember-a11y-report/*.json > ember-a11y-reports/admin.json
|
||||
jq -s 'map(.[])' ./ui/desktop/ember-a11y-report/*.json > ember-a11y-reports/desktop.json
|
||||
- name: Output ember a11y test report summary (admin)
|
||||
run: 'cat ember-a11y-reports/admin.json | jq "map({moduleName,testName,routes,urls,violations: .violations | map({id, nodes: .nodes | map({failureSummary, targets: .[].[]?.relatedNodes?.[]?.target? })}) })"'
|
||||
- name: Output ember a11y test report summary (desktop)
|
||||
run: 'cat ember-a11y-reports/desktop.json | jq "map({moduleName,testName,routes,urls,violations: .violations | map({id, nodes: .nodes | map({failureSummary, targets: .[].[]?.relatedNodes?.[]?.target? })}) })"'
|
||||
- name: Upload ember a11y reports
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ember-a11y-reports
|
||||
path: ./ember-a11y-reports
|
||||
- name: Report a11y test results status
|
||||
if: always()
|
||||
# fail CI when the combined array length of admin and desktop a11y reports is not 0
|
||||
run: |
|
||||
EMBER_A11Y_FAILURE_COUNT=$(jq -s 'map(.[]) | length' ember-a11y-reports/admin.json ember-a11y-reports/desktop.json)
|
||||
echo "$EMBER_A11Y_FAILURE_COUNT ember-a11y test failures"
|
||||
if [ "$EMBER_A11Y_FAILURE_COUNT" != '0' ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -9,91 +9,86 @@ on:
|
||||
jobs:
|
||||
dependencies:
|
||||
name: Install dependencies
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
run_install: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn install
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
build-admin-ui-oss:
|
||||
name: Build Admin UI OSS
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
run_install: false
|
||||
- name: Build
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:oss
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm run build:ui:admin:oss
|
||||
- name: Upload artifact admin-ui
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: admin-ui
|
||||
path: ./ui/admin/dist/
|
||||
- name: Upload artifact OSS admin UI
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: admin-ui-oss
|
||||
path: ./ui/admin/dist/
|
||||
build-admin-ui-enterprise:
|
||||
name: Build Admin UI Enterprise
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
run_install: false
|
||||
- name: Build
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:enterprise
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm run build:ui:admin:enterprise
|
||||
- name: Upload artifact enterprise admin UI
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: admin-ui-ent
|
||||
path: ./ui/admin/dist/
|
||||
build-admin-ui-hcp:
|
||||
name: Build Admin UI HCP
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
run_install: false
|
||||
- name: Build
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:hcp
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm run build:ui:admin:hcp
|
||||
- name: Upload artifact HCP admin UI
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: admin-ui-hcp
|
||||
path: ./ui/admin/dist/
|
||||
@@ -1,34 +0,0 @@
|
||||
name: "Trigger Build Artifacts for Testing"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: Please use tag-like format n.n.n-someword (1.111.11-beta)
|
||||
required: true
|
||||
product_version:
|
||||
required: true
|
||||
type: string
|
||||
description: The version number belonging to the oss binary to be used
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
description: The git SHA of the oss artifacts to be used
|
||||
|
||||
jobs:
|
||||
trigger-gha-build-artifacts-for-testing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Release description
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Triggering Build Artifacts for Testing using BRANCH and TEST_TAG ${{ github.event.inputs.branch }}"
|
||||
- name: Trigger Build Artifacts for Testing in boundary-ui-releases
|
||||
run: |
|
||||
export GITHUB_TOKEN="${{ secrets.ELEVATED_GITHUB_TOKEN }}"
|
||||
gh workflow run build-artifacts-for-testing.yml \
|
||||
--repo hashicorp/boundary-ui-releases \
|
||||
--ref main \
|
||||
-f TAG="${{ inputs.branch }}" \
|
||||
-f PRODUCT_VERSION="${{ inputs.product_version }}" \
|
||||
-f COMMIT="${{ inputs.commit }}" \
|
||||
@@ -17,7 +17,7 @@ on:
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -27,18 +27,18 @@ jobs:
|
||||
language: ['javascript']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0
|
||||
uses: github/codeql-action/init@70df9def86d22bf0ea4e7f8b956e7b92e7c1ea22 # codeql-bundle-v2.20.7
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
@@ -54,4 +54,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0
|
||||
uses: github/codeql-action/analyze@70df9def86d22bf0ea4e7f8b956e7b92e7c1ea22 # codeql-bundle-v2.20.7
|
||||
@@ -2,14 +2,18 @@ name: Add Copyright Headers
|
||||
on: pull_request
|
||||
jobs:
|
||||
copywrite:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
# Ref and repository aren't normally needed, this is a workaround for a bug in auto commit
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
- uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3
|
||||
- name: Add missing Copyright Headers
|
||||
run: copywrite headers
|
||||
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
|
||||
- uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1
|
||||
with:
|
||||
commit_message: Add missing copyright headers
|
||||
@@ -11,7 +11,7 @@ name: Jira Sync
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
name: Jira sync
|
||||
steps:
|
||||
- name: Check if community user
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
||||
with:
|
||||
|
||||
@@ -4,71 +4,71 @@ jobs:
|
||||
|
||||
dependencies:
|
||||
name: Install dependencies
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
run_install: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn install
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
compliance:
|
||||
name: License Compliance
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
run_install: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run compliance:licenses
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm run compliance:licenses
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: [dependencies, compliance]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
run_install: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn lint
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm lint
|
||||
|
||||
test:
|
||||
name: Test
|
||||
needs: [lint]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
id: node-modules-cache
|
||||
- uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
install-dependencies: true
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
run_install: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn test
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm test
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
trigger-oss-merge:
|
||||
# Run this only on merge events in OSS repo
|
||||
if: ${{ github.event.pull_request.merged && github.repository == 'hashicorp/boundary-ui' }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- name: Trigger Merge
|
||||
env:
|
||||
|
||||
@@ -4,10 +4,10 @@ on:
|
||||
types: [ oss-merge ]
|
||||
jobs:
|
||||
oss-merge:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
|
||||
- run: |
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
name: Test E2E Desktop Client
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, opened, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: ${{contains(github.event.pull_request.labels.*.name, 'desktop')}}
|
||||
name: Run Desktop E2E Tests
|
||||
runs-on: ${{ fromJSON(vars.RUNNER_UBUNTU_22) }}
|
||||
env:
|
||||
ENOS_VAR_boundary_edition: "ent"
|
||||
ENOS_VAR_e2e_debug_no_run: true
|
||||
ENOS_VAR_boundary_license_path: "./license.hclic"
|
||||
SETUP_CLI: true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: |
|
||||
pnpm-lock.yaml
|
||||
ui/desktop/electron-app/pnpm-lock.yaml
|
||||
|
||||
- name: Set up Terraform
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd
|
||||
|
||||
- name: Set up Enos
|
||||
uses: hashicorp/action-setup-enos@b9fa53484a1e8fdcc7b02a118bcf01d65b9414c9
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Vault CLI
|
||||
run: |
|
||||
wget https://releases.hashicorp.com/vault/1.12.2/vault_1.12.2_linux_amd64.zip -O /tmp/vault.zip
|
||||
unzip /tmp/vault.zip -d /usr/local/bin
|
||||
|
||||
- name: Checkout boundary-enterprise
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: hashicorp/boundary-enterprise
|
||||
# ref: ${{ github.event.inputs.boundary-enterprise-branch }}
|
||||
path: support/src/boundary-enterprise
|
||||
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
|
||||
|
||||
- name: GH fix for localhost resolution
|
||||
run: |
|
||||
cat /etc/hosts && echo "-----------"
|
||||
sudo sed -i 's/::1 *localhost ip6-localhost ip6-loopback/::1 ip6 -localhost ip6-loopback/g' /etc/hosts
|
||||
cat /etc/hosts
|
||||
ssh -V
|
||||
|
||||
echo "127.0.0.1 localhost boundary" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 localhost worker" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 localhost vault" | sudo tee -a /etc/hosts
|
||||
|
||||
# TODO: Build image ourselves in case we don't want to use latest?
|
||||
- name: Get Boundary docker image
|
||||
run: |
|
||||
docker pull hashicorp/boundary-enterprise:latest
|
||||
docker save hashicorp/boundary-enterprise:latest | gzip > /tmp/boundary-enterprise.zip
|
||||
|
||||
# TODO: Set up license in a better way? Use directly rather than writing to file and pull from secrets manager
|
||||
- name: Set up test infra
|
||||
id: infra
|
||||
run: |
|
||||
echo "${{ secrets.BOUNDARY_ENT_LICENSE }}" > ./support/src/boundary-enterprise/enos/license.hclic
|
||||
ssh-keygen -N '' -t ed25519 -f ~/.ssh/github_enos
|
||||
mkdir -p ./enos/terraform-plugin-cache
|
||||
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
||||
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
|
||||
export ENOS_VAR_boundary_docker_image_name=hashicorp/boundary-enterprise:latest && \
|
||||
export ENOS_VAR_boundary_docker_image_file=/tmp/boundary-enterprise.zip && \
|
||||
enos scenario launch --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:crt
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
cd e2e-tests
|
||||
pnpm playwright install --with-deps
|
||||
|
||||
# Runner is headless so we need to enable a display to run the client
|
||||
- name: Run Tests
|
||||
run: |
|
||||
source <(bash ./support/src/boundary-enterprise/enos/scripts/test_e2e_env.sh)
|
||||
cd e2e-tests
|
||||
export DISPLAY=:99
|
||||
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
|
||||
pnpm run desktop
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: playwright-report
|
||||
path: e2e-tests/playwright-report
|
||||
|
||||
- name: Clean up test infra
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
||||
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
|
||||
enos scenario destroy --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:crt
|
||||
@@ -1,25 +0,0 @@
|
||||
name: "Trigger Test Boundary UI release"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: Please use tag-like format n.n.n-someword (1.111.11-beta)
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
trigger-gha-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Release description
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Triggering Test release using BRANCH and TEST_TAG ${{ github.event.inputs.branch }}"
|
||||
- name: Trigger Release workflow in boundary-ui-releases
|
||||
run: |
|
||||
export GITHUB_TOKEN="${{ secrets.ELEVATED_GITHUB_TOKEN }}"
|
||||
gh workflow run release.yml \
|
||||
--repo hashicorp/boundary-ui-releases \
|
||||
--ref main \
|
||||
-f TAG="${{ github.event.inputs.branch }}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "Trigger Boundary UI release"
|
||||
name: "Trigger Boundary Desktop release"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,16 +7,16 @@ on:
|
||||
|
||||
jobs:
|
||||
trigger-gha-release:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Release description
|
||||
run: |
|
||||
echo "Triggering release with a TAG ${GITHUB_REF#refs/tags/}"
|
||||
- name: Trigger Release workflow in boundary-ui-releases
|
||||
- name: Trigger Release workflow in boundary-desktop-releases
|
||||
run: |
|
||||
export GITHUB_TOKEN="${{ secrets.ELEVATED_GITHUB_TOKEN }}"
|
||||
gh workflow run release.yml \
|
||||
--repo hashicorp/boundary-ui-releases \
|
||||
--repo hashicorp/boundary-desktop-releases \
|
||||
--ref main \
|
||||
-f TAG="${GITHUB_REF#refs/tags/}"
|
||||
@@ -19,7 +19,6 @@ coverage-*
|
||||
libpeerconnection.log
|
||||
npm-debug.log*
|
||||
testem.log
|
||||
yarn-error.log
|
||||
.watchman-cookie*
|
||||
|
||||
### VisualStudioCode ###
|
||||
|
||||
+2
-5
@@ -1,5 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn compliance:licenses
|
||||
npx lint-staged
|
||||
pnpm compliance:licenses
|
||||
pnpm lint-staged
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
@@ -24,4 +21,3 @@ else
|
||||
# This is to allow interaction with the terminal for commitizen
|
||||
exec < /dev/tty && npx git-cz --hook || true
|
||||
fi
|
||||
|
||||
Binary file not shown.
@@ -25,7 +25,7 @@ subproject, e.g. running or testing.
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Optional Tooling](#optional-tooling)
|
||||
- [Installation](#installation)
|
||||
- [Yarn Commands](#yarn-commands)
|
||||
- [Pnpm Commands](#pnpm-commands)
|
||||
- [Contributing](#contributing)
|
||||
- [Branching](#branching)
|
||||
- [Building ToC](#building-toc)
|
||||
@@ -33,8 +33,8 @@ subproject, e.g. running or testing.
|
||||
- [Building with a Container](#building-with-a-container)
|
||||
- [Building Desktop UI for Production](#building-desktop-ui-for-production)
|
||||
- [Connect Boundary UI to Boundary local instance](#connect-boundary-ui-to-boundary-local-instance)
|
||||
- [For admin](#for-admin)
|
||||
- [For desktop](#for-desktop)
|
||||
- [For Admin](#for-admin)
|
||||
- [For Desktop](#for-desktop)
|
||||
- [Committing](#committing)
|
||||
- [License Checking](#license-checking)
|
||||
|
||||
@@ -67,8 +67,8 @@ You will need the following things properly installed on your computer.
|
||||
|
||||
* [Git][git]
|
||||
* [Node.js][node]
|
||||
* Supported versions: `v18` and `v20`.
|
||||
* [Yarn][yarn] installed globally
|
||||
* Supported versions: `v20` and `v22`.
|
||||
* [Pnpm][pnpm] installed globally
|
||||
* [Google Chrome][chrome]
|
||||
* [Firefox][firefox]
|
||||
|
||||
@@ -90,18 +90,18 @@ that best suit you and your personal style.
|
||||
|
||||
* `git clone <repository-url>` this repository
|
||||
* `cd boundary-ui`
|
||||
* `yarn install`
|
||||
* `pnpm install`
|
||||
|
||||
## Yarn Commands
|
||||
## Pnpm Commands
|
||||
|
||||
List of available project commands. `yarn run <command-name>`
|
||||
List of available project commands. `pnpm run <command-name>`
|
||||
|
||||
| Command | Description |
|
||||
|---------------------|---|
|
||||
| build | Builds Admin UI for production. |
|
||||
| test | Runs tests in CI mode. |
|
||||
| lint | Runs ember-template-lint on all of the hbs, js, and sass files. |
|
||||
| compliance:audit | Checks for issues using `yarn audit` with moderate and above criticality.|
|
||||
| compliance:audit | Checks for issues using `pnpm audit` with moderate and above criticality.|
|
||||
| compliance:licenses | Checks that all dependencies have OSS-compatible licenses. |
|
||||
| doc:toc | Automatically generates a table of contents for the README file. |
|
||||
|
||||
@@ -114,7 +114,7 @@ We follow [GitHub Flow][github-flow].
|
||||
### Building ToC
|
||||
|
||||
To autogenerate a ToC (table of contents) for this README,
|
||||
run `yarn doc:toc`. Please update the ToC whenever editing the structure
|
||||
run `pnpm doc:toc`. Please update the ToC whenever editing the structure
|
||||
of README.
|
||||
|
||||
### Building Admin UI for Production
|
||||
@@ -126,8 +126,8 @@ necessary for your target environment, as described in the [Environment Variable
|
||||
To quickly produce a production build of Admin UI, run from this folder:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn build
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Assets are saved to `ui/admin/dist/`.
|
||||
@@ -151,20 +151,20 @@ necessary for your target environment, as described in the [Environment Variable
|
||||
To quickly produce a production build of Desktop UI, run from this folder:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn build:ui:desktop
|
||||
pnpm install
|
||||
pnpm build:ui:desktop
|
||||
```
|
||||
|
||||
In windows, UI is generated using docker to provide a stable UI across platforms.
|
||||
|
||||
```cmd
|
||||
yarn install
|
||||
pnpm install
|
||||
docker-compose -f docker-compose-embedding.yml run build-desktop-production
|
||||
yarn build:ui:desktop:app
|
||||
pnpm build:ui:desktop:app
|
||||
```
|
||||
|
||||
To provide a signing identify for macOS build, use `BOUNDARY_DESKTOP_SIGNING_IDENTITY`
|
||||
environment variable to set signing certificate name (e.g Developer ID Application: * (*)) when building desktop app using `yarn build:ui:desktop:app`.
|
||||
environment variable to set signing certificate name (e.g Developer ID Application: * (*)) when building desktop app using `pnpm build:ui:desktop:app`.
|
||||
|
||||
Assets are saved to `ui/desktop/electron-app/out/make/`.
|
||||
DMG packaged desktop UI is available at asset location as `boundary.dmg`.
|
||||
@@ -180,7 +180,7 @@ This assumes you already have up and running a Boundary dev instance in your loc
|
||||
|
||||
This assumes you are within `boundary-ui/ui/admin`.
|
||||
|
||||
You will need to turn `off` mirage, tell the UI where to find Boundary and run it: `$ ENABLE_MIRAGE=false API_HOST=http://localhost:9200 yarn start`.
|
||||
You will need to turn `off` mirage, tell the UI where to find Boundary and run it: `$ ENABLE_MIRAGE=false API_HOST=http://localhost:9200 pnpm start`.
|
||||
|
||||
Following terminal instructions, open in your browser: `http://localhost:4200/`.
|
||||
|
||||
@@ -190,7 +190,7 @@ Once you open the UI you will see the login screen, authenticate using generated
|
||||
|
||||
This assumes you are within `boundary-ui/ui/desktop`.
|
||||
|
||||
You will need to turn `off` mirage and run it: `$ ENABLE_MIRAGE=false yarn start:desktop`.
|
||||
You will need to turn `off` mirage and run it: `$ ENABLE_MIRAGE=false pnpm start:desktop`.
|
||||
|
||||
In login screen, authenticate using generated admin login name and password.
|
||||
|
||||
@@ -199,7 +199,7 @@ Enter the cluster URL of your Boundary dev instance, by default is: `http://loca
|
||||
### Committing
|
||||
|
||||
We use `husky` and `lint-staged` to ensure linting and other checks pass before a
|
||||
commit is completed. Simply do a `yarn install` to make sure the hooks are installed.
|
||||
commit is completed. Simply do a `pnpm install` to make sure the hooks are installed.
|
||||
|
||||
Use a normal `git commit` to go through the checks, if you need to skip these checks,
|
||||
use `git commit --no-verify` to bypass them. However, a consistent commit message will
|
||||
@@ -218,10 +218,10 @@ will not be accepted.
|
||||
|
||||
[git]: https://git-scm.com/
|
||||
[node]: https://nodejs.org/
|
||||
[yarn]: https://classic.yarnpkg.com/lang/en/
|
||||
[pnpm]: https://pnpm.io
|
||||
[chrome]: https://google.com/chrome/
|
||||
[firefox]: https://firefox.com/
|
||||
[yarn-workspaces]: https://classic.yarnpkg.com/en/docs/workspaces/
|
||||
[pnpm-workspaces]: https://pnpm.io/workspaces
|
||||
[homebrew]: https://brew.sh
|
||||
[warp]: https://www.warp.dev
|
||||
[vscode]: https://code.visualstudio.com/
|
||||
|
||||
@@ -12,8 +12,8 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.hbs]
|
||||
insert_final_newline = false
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
/**
|
||||
Ember CLI sends analytics information by default. The data is completely
|
||||
anonymous, but there are times when you might want to disable this behavior.
|
||||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": false,
|
||||
|
||||
/**
|
||||
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
|
||||
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
# unconventional js
|
||||
/blueprints/*/files/
|
||||
/vendor/
|
||||
|
||||
# compiled output
|
||||
/declarations/
|
||||
/dist/
|
||||
/tmp/
|
||||
|
||||
# dependencies
|
||||
/bower_components/
|
||||
/node_modules/
|
||||
|
||||
# misc
|
||||
/coverage/
|
||||
!.*
|
||||
.*/
|
||||
.eslintcache
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/bower.json.ember-try
|
||||
/npm-shrinkwrap.json.ember-try
|
||||
/package.json.ember-try
|
||||
/package-lock.json.ember-try
|
||||
/yarn.lock.ember-try
|
||||
@@ -45,6 +45,7 @@ module.exports = {
|
||||
'./blueprints/*/index.js',
|
||||
'./config/**/*.js',
|
||||
'./tests/dummy/config/**/*.js',
|
||||
'./broccoli-plugins/**/*.js',
|
||||
],
|
||||
parserOptions: {
|
||||
sourceType: 'script',
|
||||
@@ -56,7 +57,7 @@ module.exports = {
|
||||
extends: ['plugin:n/recommended'],
|
||||
},
|
||||
{
|
||||
// Test files:
|
||||
// test files
|
||||
files: ['tests/**/*-test.{js,ts}'],
|
||||
extends: ['plugin:qunit/recommended'],
|
||||
rules: {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# compiled output
|
||||
/dist/
|
||||
/declarations/
|
||||
|
||||
# dependencies
|
||||
/node_modules/
|
||||
|
||||
# misc
|
||||
/.env*
|
||||
/.pnp*
|
||||
/.eslintcache
|
||||
/coverage/
|
||||
/testem.log
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/package.json.ember-try
|
||||
|
||||
# broccoli-debug
|
||||
/DEBUG/
|
||||
+2
-12
@@ -2,11 +2,7 @@
|
||||
/dist/
|
||||
/tmp/
|
||||
|
||||
# dependencies
|
||||
/bower_components/
|
||||
|
||||
# misc
|
||||
/.bowerrc
|
||||
/.editorconfig
|
||||
/.ember-cli
|
||||
/.env*
|
||||
@@ -19,21 +15,15 @@
|
||||
/.prettierignore
|
||||
/.prettierrc.js
|
||||
/.template-lintrc.js
|
||||
/.travis.yml
|
||||
/.watchmanconfig
|
||||
/bower.json
|
||||
/CONTRIBUTING.md
|
||||
/ember-cli-build.js
|
||||
/testem.js
|
||||
/tests/
|
||||
/yarn-error.log
|
||||
/yarn.lock
|
||||
/tsconfig.declarations.json
|
||||
/tsconfig.json
|
||||
.gitkeep
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/bower.json.ember-try
|
||||
/npm-shrinkwrap.json.ember-try
|
||||
/package.json.ember-try
|
||||
/package-lock.json.ember-try
|
||||
/yarn.lock.ember-try
|
||||
@@ -1,25 +1,13 @@
|
||||
# unconventional js
|
||||
/blueprints/*/files/
|
||||
/vendor/
|
||||
|
||||
# compiled output
|
||||
/dist/
|
||||
/tmp/
|
||||
|
||||
# dependencies
|
||||
/bower_components/
|
||||
/node_modules/
|
||||
|
||||
# misc
|
||||
/coverage/
|
||||
!.*
|
||||
.eslintcache
|
||||
.lint-todo/
|
||||
.*/
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/bower.json.ember-try
|
||||
/npm-shrinkwrap.json.ember-try
|
||||
/package.json.ember-try
|
||||
/package-lock.json.ember-try
|
||||
/yarn.lock.ember-try
|
||||
@@ -3,6 +3,8 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"ignore_dirs": ["tmp", "dist"]
|
||||
"ignore_dirs": ["dist"]
|
||||
}
|
||||
+15
-15
@@ -8,7 +8,7 @@ This addon contains the API data access layer for Boundary.
|
||||
|
||||
- [Add API to an App](#add-api-to-an-app)
|
||||
- [Installation](#installation)
|
||||
- [Yarn Commands](#yarn-commands)
|
||||
- [Pnpm Commands](#pnpm-commands)
|
||||
- [Linting](#linting)
|
||||
- [Formatting](#formatting)
|
||||
- [Running tests](#running-tests)
|
||||
@@ -21,7 +21,7 @@ This addon contains the API data access layer for Boundary.
|
||||
## Add API to an App
|
||||
|
||||
Add this addon to an Ember application's `devDependencies` as:
|
||||
`"api": "*"`, for applications included in this monorepo.
|
||||
`"api": "workspace:*"`, for applications included in this monorepo.
|
||||
|
||||
Since this addon also includes Mirage mocks, be sure to install
|
||||
`ember-cli-mirage` and add the following config to your UI project:
|
||||
@@ -38,9 +38,9 @@ See monorepo README for installation instructions.
|
||||
|
||||
When manually installing addon, ensure `ember-data-fragments@5.0.0-beta.*` is installed to use api models with data fragments.
|
||||
|
||||
## Yarn Commands
|
||||
## Pnpm Commands
|
||||
|
||||
List of available project commands. `yarn run <command-name>`
|
||||
List of available project commands. `pnpm run <command-name>`
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
@@ -64,28 +64,28 @@ Additional commands in the monorepo package may affect this projects.
|
||||
|
||||
## Linting
|
||||
|
||||
* `yarn lint:fix`
|
||||
* `yarn lint:hbs`
|
||||
* `yarn lint:hbs:fix`
|
||||
* `yarn lint:js`
|
||||
* `yarn lint:js:fix`
|
||||
* `pnpm lint:fix`
|
||||
* `pnpm lint:hbs`
|
||||
* `pnpm lint:hbs:fix`
|
||||
* `pnpm lint:js`
|
||||
* `pnpm lint:js:fix`
|
||||
|
||||
## Formatting
|
||||
|
||||
Before submitting your work, be sure to run auto-formatters
|
||||
(see commands above). This helps to ensure consistency among authors.
|
||||
|
||||
* `yarn format`
|
||||
* `pnpm format`
|
||||
|
||||
## Running tests
|
||||
|
||||
* `yarn test` – Runs the test suite on the current Ember version
|
||||
* `yarn test --server` – Runs the test suite in "watch mode"
|
||||
* `yarn test:all` – Runs the test suite against multiple Ember versions
|
||||
* `pnpm test` – Runs the test suite on the current Ember version
|
||||
* `pnpm test --server` – Runs the test suite in "watch mode"
|
||||
* `pnpm test:all` – Runs the test suite against multiple Ember versions
|
||||
|
||||
## Running the dummy application
|
||||
|
||||
* `yarn start`
|
||||
* `pnpm start`
|
||||
* Visit the dummy application at [http://localhost:4200](http://localhost:4200).
|
||||
|
||||
## Contributing
|
||||
@@ -95,5 +95,5 @@ See monorepo README for more contribution instructions.
|
||||
### Building ToC
|
||||
|
||||
To autogenerate a ToC (table of contents) for this README,
|
||||
run `yarn doc:toc`. Please update the ToC whenever editing the structure
|
||||
run `pnpm doc:toc`. Please update the ToC whenever editing the structure
|
||||
of README.
|
||||
@@ -56,32 +56,33 @@ export default function setupStubs(hooks) {
|
||||
stubTypes = types;
|
||||
|
||||
types.forEach((type, i) => {
|
||||
let models;
|
||||
let resourceName;
|
||||
if (typeOf(type) === 'object') {
|
||||
models = type.func();
|
||||
resourceName = type.resource;
|
||||
} else {
|
||||
models = this.server.schema[type].all().models;
|
||||
resourceName = type;
|
||||
}
|
||||
|
||||
this.ipcStub
|
||||
.withArgs('searchCacheDaemon')
|
||||
.onCall(i)
|
||||
.returns({
|
||||
[underscore(resourceNames[singularize(resourceName)])]: models.map(
|
||||
(model) => {
|
||||
// Use internal serializer to serialize the model correctly
|
||||
// according to our mirage serializers
|
||||
const modelData =
|
||||
this.server.serializerOrRegistry.serialize(model);
|
||||
.callsFake(() => {
|
||||
let models;
|
||||
let resourceName;
|
||||
if (typeOf(type) === 'object') {
|
||||
models = type.func();
|
||||
resourceName = type.resource;
|
||||
} else {
|
||||
models = this.server.schema[type].all().models;
|
||||
resourceName = type;
|
||||
}
|
||||
|
||||
// Serialize the data properly to standard JSON as that is what
|
||||
// we're expecting from the cache daemon response
|
||||
return JSON.parse(JSON.stringify(modelData));
|
||||
},
|
||||
),
|
||||
return {
|
||||
[underscore(resourceNames[singularize(resourceName)])]:
|
||||
models.map((model) => {
|
||||
// Use internal serializer to serialize the model correctly
|
||||
// according to our mirage serializers
|
||||
const modelData =
|
||||
this.server.serializerOrRegistry.serialize(model);
|
||||
|
||||
// Serialize the data properly to standard JSON as that is what
|
||||
// we're expecting from the cache daemon response
|
||||
return JSON.parse(JSON.stringify(modelData));
|
||||
}),
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
/**
|
||||
* This test helper can help set up and cleanup the database in your tests.
|
||||
*
|
||||
* ```js
|
||||
* import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
*
|
||||
* module('Acceptance | my test', function(hooks) {
|
||||
* setupApplicationTest(hooks);
|
||||
* setupIndexedDb(hooks);
|
||||
*
|
||||
* // add your actual tests here
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param hooks
|
||||
*/
|
||||
export function setupIndexedDb(hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
const indexedDb = this.owner.lookup('service:indexed-db');
|
||||
indexedDb.setup(`test-indexed-db-${Date.now()}`);
|
||||
});
|
||||
|
||||
hooks.afterEach(async function () {
|
||||
const indexedDb = this.owner.lookup('service:indexed-db');
|
||||
|
||||
await indexedDb.db.delete();
|
||||
});
|
||||
}
|
||||
|
||||
export default setupIndexedDb;
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* This test helper can help set up and cleanup the database in your tests.
|
||||
*
|
||||
* ```js
|
||||
* import { setupSqlite } from 'api/test-support/helpers/sqlite';
|
||||
*
|
||||
* module('Acceptance | my test', function(hooks) {
|
||||
* setupApplicationTest(hooks);
|
||||
* setupSqlite(hooks);
|
||||
*
|
||||
* // add your actual tests here
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param hooks
|
||||
*/
|
||||
export function setupSqlite(hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
const sqliteDb = this.owner.lookup('service:sqlite');
|
||||
await sqliteDb.setup(`test-sqlite-${uuidv4()}`);
|
||||
});
|
||||
|
||||
hooks.afterEach(async function () {
|
||||
const sqliteDb = this.owner.lookup('service:sqlite');
|
||||
|
||||
await sqliteDb.deleteDatabase();
|
||||
});
|
||||
}
|
||||
|
||||
export default setupSqlite;
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import ModelAbility from './model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* Provides abilities for channel recordings.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import ModelAbility from './model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
/**
|
||||
* Provides abilities for credential store.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import ModelAbility from './model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
class InvalidRolePrincipalTypeError extends Error {
|
||||
name = 'InvalidRolePrincipalTypeError';
|
||||
|
||||
@@ -44,7 +44,8 @@ export default class TargetAbility extends ModelAbility {
|
||||
*/
|
||||
get canAddInjectedApplicationCredentialSources() {
|
||||
return (
|
||||
this.model.isSSH && this.hasAuthorizedAction('add-credential-sources')
|
||||
(this.model.isSSH || this.model.isRDP) &&
|
||||
this.hasAuthorizedAction('add-credential-sources')
|
||||
);
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -115,7 +115,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
* @return {Promise} promise
|
||||
*/
|
||||
async query(store, schema, query) {
|
||||
const queryObject = structuredClone(query);
|
||||
const { batchLimit, ...queryObject } = query;
|
||||
let result;
|
||||
let data = [];
|
||||
|
||||
@@ -126,8 +126,10 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
result = await super.query(store, schema, query);
|
||||
return prenormalizeArrayResponse(result);
|
||||
}
|
||||
|
||||
// Run this loop as long as the response_type is delta,
|
||||
// which indicates that there are more items in the list
|
||||
// or the batch size has not been reached
|
||||
do {
|
||||
result = await super.query(store, schema, queryObject);
|
||||
//add the result items to a data array
|
||||
@@ -136,7 +138,10 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
//pass in the list token for subsequent calls to fetch the remaining list items
|
||||
queryObject.list_token = result.list_token;
|
||||
}
|
||||
} while (result && result.response_type === 'delta');
|
||||
} while (
|
||||
result?.response_type === 'delta' &&
|
||||
(!batchLimit || data.length < batchLimit)
|
||||
);
|
||||
|
||||
result.items = data;
|
||||
return prenormalizeArrayResponse(result);
|
||||
@@ -163,7 +168,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides default ajax method by rewritting PUT requests as PATCH.
|
||||
* Overrides default ajax method by rewriting PUT requests as PATCH.
|
||||
* PATCH is the request method used by our API for updates.
|
||||
*
|
||||
* @override
|
||||
@@ -291,9 +296,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
const errors = fieldErrors.map((error) => ({
|
||||
detail: error.description,
|
||||
// Going forward, nested attributes will be hoisted. Thus "attributes."
|
||||
// must be stripped from the error name key. Until all models are
|
||||
// transitioned away from model fragments, however, both this and the
|
||||
// prior error key mappings are necessary.
|
||||
// must be stripped from the error name key.
|
||||
source: {
|
||||
pointer: `/data/attributes/${error.name.replace('attributes.', '')}`,
|
||||
},
|
||||
|
||||
@@ -76,13 +76,13 @@ export default class GeneratedCredentialLibraryModel extends BaseModel {
|
||||
path;
|
||||
|
||||
@attr('string', {
|
||||
for: 'vault-generic',
|
||||
for: ['vault-generic', 'vault-ldap'],
|
||||
description: 'It indicates the type of credential the library returns.',
|
||||
})
|
||||
credential_type;
|
||||
|
||||
@attr('object-as-array', {
|
||||
for: 'vault-generic',
|
||||
for: ['vault-generic', 'vault-ldap'],
|
||||
description: 'It indicates the credential mapping overrides.',
|
||||
})
|
||||
credential_mapping_overrides;
|
||||
|
||||
@@ -51,17 +51,17 @@ export default class GeneratedCredentialModel extends BaseModel {
|
||||
})
|
||||
version;
|
||||
|
||||
// =attributes (username_password, ssh_private_key)
|
||||
// =attributes (username_password, ssh_private_key, username_password_domain)
|
||||
@attr('string', {
|
||||
for: ['username_password', 'ssh_private_key'],
|
||||
for: ['username_password', 'ssh_private_key', 'username_password_domain'],
|
||||
isNestedAttribute: true,
|
||||
description: 'The username for credential.',
|
||||
})
|
||||
username;
|
||||
|
||||
// =attributes (username_password)
|
||||
// =attributes (username_password, username_password_domain)
|
||||
@attr('string', {
|
||||
for: 'username_password',
|
||||
for: ['username_password', 'username_password_domain'],
|
||||
isNestedAttribute: true,
|
||||
isSecret: true,
|
||||
description: 'The password for credential.',
|
||||
@@ -92,4 +92,12 @@ export default class GeneratedCredentialModel extends BaseModel {
|
||||
description: 'The secret associated with credential.',
|
||||
})
|
||||
json_object;
|
||||
|
||||
// =attributes (username_password_domain)
|
||||
@attr('string', {
|
||||
for: 'username_password_domain',
|
||||
isNestedAttribute: true,
|
||||
description: 'The domain for credential.',
|
||||
})
|
||||
domain;
|
||||
}
|
||||
@@ -55,23 +55,37 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
})
|
||||
plugin;
|
||||
|
||||
// AWS & Azure
|
||||
// AWS, Azure & GCP
|
||||
|
||||
@attr('boolean', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: ['aws', 'azure', 'gcp'],
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
disable_credential_rotation;
|
||||
|
||||
// AWS specific
|
||||
// AWS and GCP specific
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: ['aws', 'gcp'],
|
||||
},
|
||||
description:
|
||||
'An expression used to filter the workers that have network access to a service that is hosting the external object store.',
|
||||
})
|
||||
worker_filter;
|
||||
|
||||
// AWS specific
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
@@ -79,12 +93,22 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
|
||||
// AWS static credentials
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'static-credential',
|
||||
},
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
access_key_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'static-credential',
|
||||
},
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
@@ -92,24 +116,44 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
|
||||
// AWS dynamic credentials
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'dynamic-credential',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The role ARN to use.',
|
||||
})
|
||||
role_arn;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'dynamic-credential',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The role external ID to use.',
|
||||
})
|
||||
role_external_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'dynamic-credential',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The role session to use.',
|
||||
})
|
||||
role_session_name;
|
||||
|
||||
@attr('object-as-array', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'dynamic-credential',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The role tags to use.',
|
||||
})
|
||||
@@ -118,32 +162,117 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
// Azure specific
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
tenant_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
client_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
subscription_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
secret_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
secret_value;
|
||||
|
||||
// GCP specific
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The project ID associated with the service account.',
|
||||
})
|
||||
project_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The deployment area within a region.',
|
||||
})
|
||||
zone;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description:
|
||||
'The email address associated with the service account. The email address used to uniquely identify the service account. It is required for authentication and authorization.',
|
||||
})
|
||||
client_email;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description:
|
||||
'The unique identifier for the service account that will be impersonated.',
|
||||
})
|
||||
target_service_account_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
description:
|
||||
'The ID of the private key used to sign the JWT and authentication.',
|
||||
isNestedSecret: true,
|
||||
})
|
||||
private_key_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
description:
|
||||
'The private key used to sign the JWT and obtain a OAuth 2.0 access token.',
|
||||
isNestedSecret: true,
|
||||
})
|
||||
private_key;
|
||||
}
|
||||
@@ -63,6 +63,10 @@ export default class GeneratedHostSetModel extends BaseModel {
|
||||
plugin;
|
||||
|
||||
@attr('number', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: ['aws', 'azure', 'gcp'],
|
||||
},
|
||||
description:
|
||||
"The number of seconds between the time boundary syncs the hosts in this set using this host set's plugin. If not provided a system determined default is used",
|
||||
})
|
||||
@@ -79,12 +83,21 @@ export default class GeneratedHostSetModel extends BaseModel {
|
||||
host_ids;
|
||||
|
||||
@attr('string-array', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: ['aws', 'azure', 'gcp'],
|
||||
},
|
||||
emptyArrayIfMissing: true,
|
||||
trimWhitespace: true,
|
||||
})
|
||||
preferred_endpoints;
|
||||
|
||||
// AWS specific
|
||||
// AWS & GCP specific
|
||||
@attr('string-array', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: ['aws', 'gcp'],
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
emptyArrayIfMissing: true,
|
||||
})
|
||||
@@ -92,6 +105,10 @@ export default class GeneratedHostSetModel extends BaseModel {
|
||||
|
||||
// Azure specific, comes in from API as "filter". This is to avoid collisions with filter
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
description: '',
|
||||
isNestedAttribute: true,
|
||||
})
|
||||
|
||||
@@ -70,6 +70,7 @@ export default class GeneratedStorageBucketModel extends BaseModel {
|
||||
@attr('string', {
|
||||
isNestedAttribute: true,
|
||||
description: 'The AWS region to use.',
|
||||
trimWhitespace: true,
|
||||
})
|
||||
region;
|
||||
|
||||
@@ -109,6 +110,7 @@ export default class GeneratedStorageBucketModel extends BaseModel {
|
||||
@attr('string', {
|
||||
isNestedAttribute: true,
|
||||
description: 'The role ARN to use.',
|
||||
trimWhitespace: true,
|
||||
})
|
||||
role_arn;
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
/* global __electronLog */
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/application';
|
||||
import { generateMQLExpression } from '../utils/mql-query';
|
||||
import { paginateResults } from '../utils/paginate-results';
|
||||
import { underscore } from '@ember/string';
|
||||
|
||||
const { __electronLog } = globalThis;
|
||||
|
||||
/**
|
||||
* Not all types are yet supported by the cache daemon so we'll
|
||||
* just whitelist the ones we need to start with. Keys are the resource
|
||||
@@ -42,7 +43,10 @@ const fetchControllerData = async (context, next) => {
|
||||
// If we get an error or cache daemon is unavailable, fall back to calling the API
|
||||
context.request.data.query = remainingQuery;
|
||||
const results = await next(context.request);
|
||||
const models = results.content.toArray();
|
||||
|
||||
// As a result of https://rfcs.emberjs.com/id/0846-ember-data-deprecate-proxies/ we cannot use proxy array methods like .toArray
|
||||
// use slice() instead as indicated in the RFC.
|
||||
const models = results.content.slice();
|
||||
|
||||
const paginatedResult = paginateResults(models, page, pageSize);
|
||||
paginatedResult.meta = { totalItems: models.length };
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { modelIndexes } from 'api/services/indexed-db';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/application';
|
||||
import { queryIndexedDb } from '../utils/indexed-db-query';
|
||||
import { paginateResults } from '../utils/paginate-results';
|
||||
import { hashCode } from '../utils/hash-code';
|
||||
|
||||
/**
|
||||
* Handler to sit in front of the API request layer
|
||||
* so we can handle any caching with indexedDb first
|
||||
*/
|
||||
export default class IndexedDbHandler {
|
||||
@service indexedDb;
|
||||
|
||||
constructor(context) {
|
||||
setOwner(this, getOwner(context));
|
||||
}
|
||||
|
||||
async request(context, next) {
|
||||
switch (context.request.op) {
|
||||
case 'query': {
|
||||
const { store, data } = context.request;
|
||||
// TODO: Remove storeToken option as this is a temporary fix for auth-methods.
|
||||
const {
|
||||
type,
|
||||
query,
|
||||
options: {
|
||||
pushToStore = true,
|
||||
peekIndexedDB = false,
|
||||
storeToken = true,
|
||||
} = {},
|
||||
} = data;
|
||||
const supportedModels = Object.keys(modelIndexes);
|
||||
const { db: indexedDb } = this.indexedDb ?? {};
|
||||
|
||||
// Go through normal flow if we don't yet support the model
|
||||
// or if we don't have an indexedDb instance
|
||||
if (!supportedModels.includes(type) || !indexedDb) {
|
||||
return next(context.request);
|
||||
}
|
||||
|
||||
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
|
||||
|
||||
const adapter = store.adapterFor(type);
|
||||
const schema = store.modelFor(type);
|
||||
const serializer = store.serializerFor(type);
|
||||
|
||||
if (!peekIndexedDB) {
|
||||
const tokenKey = `${type}-${hashCode(remainingQuery)}`;
|
||||
|
||||
let listToken;
|
||||
if (storeToken) {
|
||||
listToken = await indexedDb.token.get(tokenKey);
|
||||
}
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
list_token: listToken?.token,
|
||||
});
|
||||
} catch (e) {
|
||||
// If we get an invalid list token, we'll delete the token and
|
||||
// clear the DB and try again without a token.
|
||||
if (
|
||||
e?.errors[0].status === 400 &&
|
||||
e?.errors[0].code === 'invalid list token'
|
||||
) {
|
||||
await indexedDb[type].clear();
|
||||
// Delete all tokens of the same resource type so we don't keep clearing the DB.
|
||||
// Even if other tokens may still be valid, we might have cleared data that they
|
||||
// depended on so we should clear all tokens of the same type when clearing the DB.
|
||||
const tokenKeys = await indexedDb.token
|
||||
.where(':id')
|
||||
.startsWith(type)
|
||||
.primaryKeys();
|
||||
|
||||
await indexedDb.token.bulkDelete(tokenKeys);
|
||||
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Store the token we just got back from the payload if it exists
|
||||
if (payload.list_token && storeToken) {
|
||||
await indexedDb.token.put({
|
||||
id: tokenKey,
|
||||
token: payload.list_token,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove any records from the DB if the API indicates they've been deleted
|
||||
if (payload.removed_ids?.length > 0) {
|
||||
await indexedDb[type].bulkDelete(payload.removed_ids);
|
||||
}
|
||||
// This is a temporary fix of clearing the DB (specifically for auth-methods)
|
||||
// since we are not storing the token we do not get back a list of removed_ids
|
||||
// from the API call and we do not want deleted items from showing in list view.
|
||||
if (!storeToken) {
|
||||
await indexedDb[type].clear();
|
||||
}
|
||||
|
||||
const normalizedPayload = serializer.normalizeResponse(
|
||||
store,
|
||||
schema,
|
||||
payload,
|
||||
null,
|
||||
'query',
|
||||
);
|
||||
|
||||
const { data: payloadData } = normalizedPayload;
|
||||
|
||||
// Store the new data we just got back from the API refresh
|
||||
const items = payloadData.map((datum) =>
|
||||
this.indexedDb.normalizeData({
|
||||
data: datum,
|
||||
cleanData: true,
|
||||
schema,
|
||||
serializer,
|
||||
}),
|
||||
);
|
||||
|
||||
await indexedDb[type].bulkPut(items);
|
||||
}
|
||||
|
||||
const indexedDbResults = await queryIndexedDb(
|
||||
indexedDb,
|
||||
type,
|
||||
queryObj,
|
||||
);
|
||||
|
||||
const dbRecords = paginateResults(indexedDbResults, page, pageSize).map(
|
||||
(item) =>
|
||||
this.indexedDb.normalizeData({
|
||||
data: item,
|
||||
cleanData: false,
|
||||
schema,
|
||||
serializer,
|
||||
}),
|
||||
);
|
||||
|
||||
// Return the raw data if we don't push to the store.
|
||||
let records = dbRecords.map((record) => ({
|
||||
...record.attributes,
|
||||
id: record.id,
|
||||
}));
|
||||
|
||||
if (pushToStore) {
|
||||
records = store.push({ data: dbRecords });
|
||||
}
|
||||
|
||||
// Set a meta property on the array to store the total items of the results.
|
||||
// This isn't conventional but is better than returning an ArrayProxy
|
||||
// or EmberArray since the ember store query method asserts it has to be an array
|
||||
// so we can't just return an object.
|
||||
records.meta = { totalItems: indexedDbResults.length };
|
||||
return records;
|
||||
}
|
||||
default:
|
||||
return next(context.request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { getOwner, setOwner } from '@ember/owner';
|
||||
import { service } from '@ember/service';
|
||||
import { modelMapping } from 'api/services/sqlite';
|
||||
import { get } from '@ember/object';
|
||||
import { typeOf } from '@ember/utils';
|
||||
import { hashCode } from '../utils/hash-code';
|
||||
import { generateSQLExpressions } from '../utils/sqlite-query';
|
||||
|
||||
const isISODateString = (str) =>
|
||||
typeOf(str) === 'string' &&
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z?$/.test(str);
|
||||
|
||||
export default class SqliteHandler {
|
||||
@service sqlite;
|
||||
|
||||
batchLimit = 5_000;
|
||||
|
||||
constructor(context) {
|
||||
setOwner(this, getOwner(context));
|
||||
}
|
||||
|
||||
async request(context, next) {
|
||||
switch (context.request.op) {
|
||||
case 'query': {
|
||||
const { store, data } = context.request;
|
||||
const {
|
||||
type,
|
||||
query,
|
||||
options: {
|
||||
pushToStore = true,
|
||||
peekDb = false,
|
||||
storeToken = true,
|
||||
} = {},
|
||||
} = data;
|
||||
const supportedModels = Object.keys(modelMapping);
|
||||
|
||||
// Go through normal flow if we don't yet support the model
|
||||
// or if we don't have a sqlite db instance
|
||||
if (!supportedModels.includes(type) || !this.sqlite.worker) {
|
||||
return next(context.request);
|
||||
}
|
||||
|
||||
const adapter = store.adapterFor(type);
|
||||
const schema = store.modelFor(type);
|
||||
const serializer = store.serializerFor(type);
|
||||
|
||||
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
|
||||
let payload,
|
||||
listToken,
|
||||
writeToDbPromise,
|
||||
totalInsert = 0;
|
||||
|
||||
if (!peekDb) {
|
||||
const tokenKey = `${type}-${hashCode(remainingQuery)}`;
|
||||
if (storeToken) {
|
||||
const [tokenObj] = await this.sqlite.fetchResource({
|
||||
...generateSQLExpressions('token', {
|
||||
filters: { id: [{ equals: tokenKey }] },
|
||||
}),
|
||||
});
|
||||
listToken = tokenObj?.token;
|
||||
} else {
|
||||
// This is a temporary fix of clearing the DB (specifically for auth-methods)
|
||||
// since we are not storing the token we do not get back a list of removed_ids
|
||||
// from the API call and we do not want deleted items from showing in list view.
|
||||
await this.sqlite.deleteResource(type);
|
||||
}
|
||||
|
||||
do {
|
||||
try {
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
list_token: listToken,
|
||||
batchLimit: this.batchLimit,
|
||||
});
|
||||
totalInsert += payload.items?.length ?? 0;
|
||||
|
||||
// await the previous writeToDbPromise before writing to db again
|
||||
if (writeToDbPromise) {
|
||||
await writeToDbPromise;
|
||||
}
|
||||
} catch (err) {
|
||||
payload = await this.retryQueryFailure({
|
||||
err,
|
||||
type,
|
||||
adapter,
|
||||
store,
|
||||
schema,
|
||||
remainingQuery,
|
||||
});
|
||||
totalInsert += payload.items?.length ?? 0;
|
||||
}
|
||||
listToken = payload.list_token;
|
||||
|
||||
writeToDbPromise = this.writeToDb(
|
||||
payload,
|
||||
storeToken,
|
||||
tokenKey,
|
||||
serializer,
|
||||
store,
|
||||
schema,
|
||||
type,
|
||||
);
|
||||
} while (payload.response_type === 'delta');
|
||||
|
||||
await writeToDbPromise;
|
||||
}
|
||||
|
||||
if (totalInsert > 0) {
|
||||
// If there were any inserts, let sqlite handle running analyze on the DB
|
||||
await this.sqlite.analyzeDatabase();
|
||||
}
|
||||
|
||||
const { sql, parameters } = generateSQLExpressions(type, queryObj, {
|
||||
page,
|
||||
pageSize,
|
||||
select: ['data'],
|
||||
});
|
||||
|
||||
const rows = await this.sqlite.fetchResource({
|
||||
sql,
|
||||
parameters,
|
||||
});
|
||||
|
||||
const { sql: countSql, parameters: countParams } =
|
||||
generateSQLExpressions(type, queryObj, {
|
||||
select: ['count(*) as total'],
|
||||
});
|
||||
const count = await this.sqlite.fetchResource({
|
||||
sql: countSql,
|
||||
parameters: countParams,
|
||||
});
|
||||
|
||||
const results = rows.map((item) =>
|
||||
JSON.parse(item.data, (key, value) =>
|
||||
// Reviver function to convert any ISO strings back to date objects
|
||||
// as we had to initially convert them to strings when stored in SQLite
|
||||
isISODateString(value) ? new Date(value) : value,
|
||||
),
|
||||
);
|
||||
|
||||
// If we are not pushing to the store, use the raw data with id property
|
||||
const records = pushToStore
|
||||
? store.push({ data: results })
|
||||
: results.map((record) => ({
|
||||
...record.attributes,
|
||||
id: record.id,
|
||||
}));
|
||||
|
||||
// Set a meta property on the array to store the total items of the results.
|
||||
// This isn't conventional but is better than returning an ArrayProxy
|
||||
// or EmberArray since the ember store query method asserts it has to be an array
|
||||
// so we can't just return an object.
|
||||
records.meta = { totalItems: count[0].total };
|
||||
return records;
|
||||
}
|
||||
default:
|
||||
return next(context.request);
|
||||
}
|
||||
}
|
||||
|
||||
async retryQueryFailure({
|
||||
err,
|
||||
type,
|
||||
adapter,
|
||||
store,
|
||||
schema,
|
||||
remainingQuery,
|
||||
}) {
|
||||
// If we get an invalid list token, we'll delete the token and
|
||||
// clear the DB and try again without a token.
|
||||
if (
|
||||
err?.errors?.[0].status === 400 &&
|
||||
err?.errors?.[0].code === 'invalid list token'
|
||||
) {
|
||||
await this.sqlite.deleteResource(type);
|
||||
// Delete all tokens of the same resource type so we don't keep clearing the DB.
|
||||
// Even if other tokens may still be valid, we might have cleared data that they
|
||||
// depended on so we should clear all tokens of the same type when clearing the DB.
|
||||
const { sql, parameters } = generateSQLExpressions('token', {
|
||||
filters: { id: [{ contains: type }] },
|
||||
});
|
||||
const tokenRows = await this.sqlite.fetchResource({
|
||||
sql,
|
||||
parameters,
|
||||
});
|
||||
const tokenIds = tokenRows.map((row) => row.id);
|
||||
|
||||
if (tokenIds.length > 0) {
|
||||
await this.sqlite.deleteResource('token', tokenIds);
|
||||
}
|
||||
|
||||
return await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async writeToDb(
|
||||
payload,
|
||||
storeToken,
|
||||
tokenKey,
|
||||
serializer,
|
||||
store,
|
||||
schema,
|
||||
type,
|
||||
) {
|
||||
if (payload.list_token && storeToken) {
|
||||
await this.sqlite.insertResource('token', [
|
||||
[tokenKey, payload.list_token],
|
||||
]);
|
||||
}
|
||||
|
||||
// Remove any records from the DB if the API indicates they've been deleted
|
||||
if (payload.removed_ids?.length > 0) {
|
||||
await this.sqlite.deleteResource(type, payload.removed_ids);
|
||||
}
|
||||
|
||||
const normalizedPayload = serializer.normalizeResponse(
|
||||
store,
|
||||
schema,
|
||||
payload,
|
||||
null,
|
||||
'query',
|
||||
);
|
||||
|
||||
const { data: payloadData } = normalizedPayload;
|
||||
|
||||
// Store the new data we just got back from the API refresh
|
||||
const items = payloadData.map((datum) => {
|
||||
const params = Object.values(modelMapping[type]).map((key) => {
|
||||
let value;
|
||||
|
||||
if (key === 'id') {
|
||||
value = datum.id;
|
||||
} else {
|
||||
value = get(datum, `attributes.${key}`);
|
||||
}
|
||||
|
||||
if (typeOf(value) === 'date') {
|
||||
return value.toISOString();
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
return [...params, JSON.stringify(datum)];
|
||||
});
|
||||
|
||||
await this.sqlite.insertResource(type, items);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
import Mixin from '@ember/object/mixin';
|
||||
|
||||
/**
|
||||
* Overrides URL-building methods in an adpater for greater flexibility.
|
||||
* Overrides URL-building methods in an adapter for greater flexibility.
|
||||
* Specifically enhances calls to `urlPrefix` with `modelName`, `id`, and
|
||||
* `snapshot`, which aren't normally passed. And adds support for a
|
||||
* `urlSuffix` method, the return value of which is appended to the end of
|
||||
@@ -22,7 +22,7 @@ export default Mixin.create({
|
||||
|
||||
/**
|
||||
* This override behaves identically to the default method by default.
|
||||
* It exists only to document the additional paramters to the method.
|
||||
* It exists only to document the additional parameters to the method.
|
||||
* @override
|
||||
* @param {string} path
|
||||
* @param {string} parentURL
|
||||
|
||||
@@ -47,8 +47,8 @@ export default class AccountModel extends GeneratedAccountModel {
|
||||
* @type {string}
|
||||
*/
|
||||
get accountName() {
|
||||
const { email, full_name, login_name } = this;
|
||||
return email || full_name || login_name;
|
||||
const { email, full_name, login_name, subject } = this;
|
||||
return email || full_name || login_name || subject;
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import Model from '@ember-data/model';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* Base model class provides common functionality for all models.
|
||||
|
||||
@@ -4,14 +4,22 @@
|
||||
*/
|
||||
|
||||
import GeneratedCredentialLibraryModel from '../generated/models/credential-library';
|
||||
|
||||
import {
|
||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||
} from 'api/models/credential';
|
||||
/**
|
||||
* Enum options for credential library.
|
||||
*/
|
||||
export const options = {
|
||||
http_method: ['GET', 'POST'],
|
||||
key_type: ['ed25519', 'ecdsa', 'rsa'],
|
||||
credential_types: ['ssh_private_key', 'username_password'],
|
||||
credential_types: [
|
||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||
],
|
||||
mapping_overrides: {
|
||||
username_password: ['username_attribute', 'password_attribute'],
|
||||
ssh_private_key: [
|
||||
@@ -19,12 +27,18 @@ export const options = {
|
||||
'private_key_passphrase_attribute',
|
||||
'username_attribute',
|
||||
],
|
||||
username_password_domain: [
|
||||
'username_attribute',
|
||||
'password_attribute',
|
||||
'domain_attribute',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC = 'vault-generic';
|
||||
export const TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE =
|
||||
'vault-ssh-certificate';
|
||||
export const TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP = 'vault-ldap';
|
||||
|
||||
/**
|
||||
* Supported Credential Library types.
|
||||
@@ -32,6 +46,7 @@ export const TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE =
|
||||
export const TYPES_CREDENTIAL_LIBRARY = Object.freeze([
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
|
||||
]);
|
||||
|
||||
export default class CredentialLibraryModel extends GeneratedCredentialLibraryModel {
|
||||
@@ -52,7 +67,8 @@ export default class CredentialLibraryModel extends GeneratedCredentialLibraryMo
|
||||
get isVault() {
|
||||
return (
|
||||
this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC ||
|
||||
this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE
|
||||
this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE ||
|
||||
this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,4 +87,12 @@ export default class CredentialLibraryModel extends GeneratedCredentialLibraryMo
|
||||
get isVaultSSHCertificate() {
|
||||
return this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if credential is a vault ldap type.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isVaultLDAP() {
|
||||
return this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,18 @@ import GeneratedCredentialModel from '../generated/models/credential';
|
||||
/**
|
||||
* Supported Credential types.
|
||||
*/
|
||||
export const types = ['username_password', 'ssh_private_key', 'json'];
|
||||
|
||||
export const TYPE_CREDENTIAL_USERNAME_PASSWORD = 'username_password';
|
||||
export const TYPE_CREDENTIAL_SSH_PRIVATE_KEY = 'ssh_private_key';
|
||||
export const TYPE_CREDENTIAL_JSON = 'json';
|
||||
export const TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN = 'username_password_domain';
|
||||
|
||||
export const TYPES_CREDENTIAL = Object.freeze([
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||
TYPE_CREDENTIAL_JSON,
|
||||
]);
|
||||
|
||||
export default class CredentialModel extends GeneratedCredentialModel {
|
||||
// =attributes
|
||||
@@ -28,7 +39,7 @@ export default class CredentialModel extends GeneratedCredentialModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isJSON() {
|
||||
return this.type === 'json';
|
||||
return this.type === TYPE_CREDENTIAL_JSON;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,6 +47,6 @@ export default class CredentialModel extends GeneratedCredentialModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUnknown() {
|
||||
return !types.includes(this.type);
|
||||
return !TYPES_CREDENTIAL.includes(this.type);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import GeneratedGroupModel from '../generated/models/group';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { A } from '@ember/array';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class GroupModel extends GeneratedGroupModel {
|
||||
// =services
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import GeneratedHostCatalogModel from '../generated/models/host-catalog';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export const TYPE_HOST_CATALOG_STATIC = 'static';
|
||||
export const TYPE_HOST_CATALOG_DYNAMIC = 'plugin';
|
||||
@@ -14,9 +15,11 @@ export const TYPES_HOST_CATALOG = Object.freeze([
|
||||
|
||||
export const TYPE_HOST_CATALOG_PLUGIN_AWS = 'aws';
|
||||
export const TYPE_HOST_CATALOG_PLUGIN_AZURE = 'azure';
|
||||
export const TYPE_HOST_CATALOG_PLUGIN_GCP = 'gcp';
|
||||
export const TYPES_HOST_CATALOG_PLUGIN = [
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
];
|
||||
|
||||
export const TYPE_CREDENTIAL_STATIC = 'static-credential';
|
||||
@@ -27,10 +30,19 @@ export const TYPES_CREDENTIALS = Object.freeze([
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
]);
|
||||
|
||||
export const DYNAMIC_CREDENTIAL_FIELDS = [
|
||||
'role_arn',
|
||||
'role_external_id',
|
||||
'role_session_name',
|
||||
'role_tags',
|
||||
];
|
||||
export const STATIC_CREDENTIAL_FIELDS = ['access_key_id, secret_access_key'];
|
||||
|
||||
export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
// =attributes
|
||||
|
||||
#credentialType;
|
||||
@tracked _credentialTypeValue;
|
||||
|
||||
/**
|
||||
* True if the host catalog is static.
|
||||
* @type {boolean}
|
||||
@@ -52,14 +64,14 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
* @type {string}
|
||||
*/
|
||||
get credentialType() {
|
||||
if (!this.#credentialType) {
|
||||
if (this.role_arn) {
|
||||
this.#credentialType = TYPE_CREDENTIAL_DYNAMIC;
|
||||
if (!this._credentialTypeValue) {
|
||||
if (DYNAMIC_CREDENTIAL_FIELDS.some((field) => this[field])) {
|
||||
return TYPE_CREDENTIAL_DYNAMIC;
|
||||
} else {
|
||||
this.#credentialType = TYPE_CREDENTIAL_STATIC;
|
||||
return TYPE_CREDENTIAL_STATIC;
|
||||
}
|
||||
}
|
||||
return this.#credentialType;
|
||||
return this._credentialTypeValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +79,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
* @type {string}
|
||||
*/
|
||||
set credentialType(type) {
|
||||
this.#credentialType = type;
|
||||
this._credentialTypeValue = type;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +97,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isAWS() {
|
||||
return this.compositeType === 'aws';
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AWS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +105,15 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isAzure() {
|
||||
return this.compositeType === 'azure';
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if host catalog plugin type is GCP.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isGCP() {
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_GCP;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
*/
|
||||
|
||||
import GeneratedHostSetModel from '../generated/models/host-set';
|
||||
import { TYPES_HOST_CATALOG_PLUGIN } from './host-catalog';
|
||||
import {
|
||||
TYPES_HOST_CATALOG_PLUGIN,
|
||||
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
} from './host-catalog';
|
||||
|
||||
export default class HostSetModel extends GeneratedHostSetModel {
|
||||
// =attributes
|
||||
@@ -40,14 +45,22 @@ export default class HostSetModel extends GeneratedHostSetModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isAWS() {
|
||||
return this.compositeType === 'aws';
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AWS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if a host-set plugin is Azure or not.
|
||||
*/
|
||||
get isAzure() {
|
||||
return this.compositeType === 'azure';
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if host catalog plugin type is GCP.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isGCP() {
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_GCP;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import GeneratedRoleModel from '../generated/models/role';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export const GRANT_SCOPE_THIS = 'this';
|
||||
export const GRANT_SCOPE_CHILDREN = 'children';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import GeneratedSessionRecordingModel from '../generated/models/session-recording';
|
||||
import { hasMany } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export const TYPE_SESSION_RECORDING_SSH = 'ssh';
|
||||
export const TYPES_SESSION_RECORDING = Object.freeze([
|
||||
@@ -71,7 +71,15 @@ export default class SessionRecordingModel extends GeneratedSessionRecordingMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Reapply policy dates via the `reapply-staorage-policy` method.
|
||||
* Returns true if retention of session-recording is forever.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get retainForever() {
|
||||
return this.retain_until?.toISOString() === '9999-12-31T23:23:23.999Z';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reapply policy dates via the `reapply-storage-policy` method.
|
||||
* @param {object} options
|
||||
* @param {object} options.adapterOptions
|
||||
* @return {Promise}
|
||||
|
||||
@@ -7,7 +7,10 @@ import GeneratedSessionModel from '../generated/models/session';
|
||||
import { equal } from '@ember/object/computed';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { A } from '@ember/array';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
import { flattenObject } from '../utils/flatten-nested-object';
|
||||
import { TYPES_CREDENTIAL_LIBRARY } from 'api/models/credential-library';
|
||||
import { TYPE_CREDENTIAL_JSON } from 'api/models/credential';
|
||||
|
||||
export const STATUS_SESSION_ACTIVE = 'active';
|
||||
export const STATUS_SESSION_PENDING = 'pending';
|
||||
@@ -23,7 +26,7 @@ export const statusTypes = [
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class SessionCredential {
|
||||
export class SessionCredential {
|
||||
// =classes
|
||||
|
||||
/**
|
||||
@@ -34,12 +37,14 @@ class SessionCredential {
|
||||
name;
|
||||
description;
|
||||
type;
|
||||
credentialtype;
|
||||
|
||||
constructor(id, name, description, type) {
|
||||
constructor(id, name, description, type, credentialtype) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.type = type;
|
||||
this.credentialType = credentialtype;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,8 +80,10 @@ class SessionCredential {
|
||||
get secrets() {
|
||||
if (this.#payloadSecret?.decoded) {
|
||||
const secretJSON = this.#payloadSecret.decoded;
|
||||
return Object.keys(secretJSON).map(
|
||||
(key) => new SessionCredential.SecretItem(key, secretJSON[key]),
|
||||
return this.extractSecrets(
|
||||
secretJSON,
|
||||
this.source.type,
|
||||
this.source.credentialType,
|
||||
);
|
||||
} else {
|
||||
// decode from base64
|
||||
@@ -85,11 +92,41 @@ class SessionCredential {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts secrets from the payload secret JSON object.
|
||||
* We only need to flatten the object if the type is vault or if its a json type credential.
|
||||
* @param {object} secretJSON - The payload secret JSON object.
|
||||
* @param {string} type - The credential source type.
|
||||
* @param {string} credentialType - The credential type.
|
||||
* @returns {SessionCredential.SecretItem[]} - The array of secret items.
|
||||
*/
|
||||
extractSecrets(secretJSON, type, credentialType) {
|
||||
let source;
|
||||
if (credentialType === TYPE_CREDENTIAL_JSON) {
|
||||
source = flattenObject(secretJSON);
|
||||
} else if (TYPES_CREDENTIAL_LIBRARY.includes(type) && secretJSON?.data) {
|
||||
source = flattenObject(secretJSON.data);
|
||||
} else {
|
||||
source = secretJSON;
|
||||
}
|
||||
|
||||
return Object.entries(source).map(
|
||||
([key, value]) => new SessionCredential.SecretItem(key, value),
|
||||
);
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
constructor(cred) {
|
||||
const { id, name, description, type } = cred.credential_source;
|
||||
this.source = new SessionCredential.Source(id, name, description, type);
|
||||
const { id, name, description, type, credential_type } =
|
||||
cred.credential_source;
|
||||
this.source = new SessionCredential.Source(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
credential_type,
|
||||
);
|
||||
this.#payloadSecret = cred.secret;
|
||||
this.rawCredential = cred;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import GeneratedStorageBucketModel from '../generated/models/storage-bucket';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export const TYPE_STORAGE_BUCKET_PLUGIN = 'plugin';
|
||||
export const TYPES_STORAGE_BUCKET = Object.freeze([TYPE_STORAGE_BUCKET_PLUGIN]);
|
||||
@@ -26,7 +27,8 @@ export const TYPES_CREDENTIALS = Object.freeze([
|
||||
export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
||||
// attributes
|
||||
|
||||
#credentialType;
|
||||
@tracked _credentialTypeValue;
|
||||
|
||||
/**
|
||||
* True if the storage bucket is a plugin.
|
||||
* @type {boolean}
|
||||
@@ -40,14 +42,14 @@ export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
||||
* @type {string}
|
||||
*/
|
||||
get credentialType() {
|
||||
if (!this.#credentialType) {
|
||||
if (!this._credentialTypeValue) {
|
||||
if (this.role_arn) {
|
||||
this.#credentialType = TYPE_CREDENTIAL_DYNAMIC;
|
||||
return TYPE_CREDENTIAL_DYNAMIC;
|
||||
} else {
|
||||
this.#credentialType = TYPE_CREDENTIAL_STATIC;
|
||||
return TYPE_CREDENTIAL_STATIC;
|
||||
}
|
||||
}
|
||||
return this.#credentialType;
|
||||
return this._credentialTypeValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,8 +57,9 @@ export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
||||
* @type {string}
|
||||
*/
|
||||
set credentialType(type) {
|
||||
this.#credentialType = type;
|
||||
this._credentialTypeValue = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the storage bucket is an unknown type.
|
||||
* @type {boolean}
|
||||
|
||||
@@ -5,11 +5,17 @@
|
||||
|
||||
import GeneratedTargetModel from '../generated/models/target';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export const TYPE_TARGET_TCP = 'tcp';
|
||||
export const TYPE_TARGET_SSH = 'ssh';
|
||||
export const TYPES_TARGET = Object.freeze([TYPE_TARGET_TCP, TYPE_TARGET_SSH]);
|
||||
export const TYPE_TARGET_RDP = 'rdp';
|
||||
|
||||
export const TYPES_TARGET = Object.freeze([
|
||||
TYPE_TARGET_TCP,
|
||||
TYPE_TARGET_SSH,
|
||||
TYPE_TARGET_RDP,
|
||||
]);
|
||||
|
||||
export default class TargetModel extends GeneratedTargetModel {
|
||||
// =services
|
||||
@@ -347,4 +353,12 @@ export default class TargetModel extends GeneratedTargetModel {
|
||||
get isSSH() {
|
||||
return this.type === TYPE_TARGET_SSH;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the target type is rdp.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isRDP() {
|
||||
return this.type === TYPE_TARGET_RDP;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
import RESTSerializer from '@ember-data/serializer/rest';
|
||||
import { underscore } from '@ember/string';
|
||||
import { get } from '@ember/object';
|
||||
import { typeOf } from '@ember/utils';
|
||||
|
||||
/**
|
||||
* Manages serialization/normalization of data to/from the API.
|
||||
@@ -42,7 +43,7 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
*/
|
||||
serializeAttribute(snapshot, json, key, attribute) {
|
||||
const { type, options } = attribute;
|
||||
const { type: recordType } = snapshot.record;
|
||||
const { type: recordType, compositeType } = snapshot.record;
|
||||
let value = super.serializeAttribute(...arguments);
|
||||
// Remove secret attributes that are null or empty
|
||||
if (options.isSecret) {
|
||||
@@ -59,6 +60,18 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
// Do not serialize `disabled` fields.
|
||||
// TODO: disabled is temporarily disabled
|
||||
if (key === 'disabled') delete json[key];
|
||||
|
||||
// Before we transform our json
|
||||
// Let's trim the values in the json obj
|
||||
// Currently, json values will only ever be a string or an array
|
||||
if (options.trimWhitespace && json[key]) {
|
||||
if (typeOf(json[key]) === 'array') {
|
||||
json[key] = json[key].map((str) => str.trim());
|
||||
} else if (typeOf(json[key] === 'string')) {
|
||||
json[key] = json[key].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Push nested attributes down into the attributes key
|
||||
if (options.isNestedAttribute && json[key] !== undefined) {
|
||||
if (!json.attributes) json.attributes = {};
|
||||
@@ -74,18 +87,38 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
delete json[key];
|
||||
}
|
||||
// If an attribute has a `for` option, it must match the
|
||||
// record's `type`, else the attribute is excluded
|
||||
// record's `type`, otherwise the attribute is excluded
|
||||
// from serialization.
|
||||
//note: This doesn't handle secrets yet, we can add support for them if needed.
|
||||
if (
|
||||
options?.for &&
|
||||
options.for !== recordType &&
|
||||
!options.for.includes(recordType)
|
||||
) {
|
||||
if (options.isNestedAttribute) {
|
||||
delete json?.attributes?.[key];
|
||||
} else {
|
||||
delete json[key];
|
||||
|
||||
if (options?.for) {
|
||||
// Check the typeof the `for` option to determine if it is a plugin or not
|
||||
const isPlugin = options.for?.type === 'plugin';
|
||||
// Helper function to handle deleting attribute and secret keys
|
||||
const deleteKey = (json) => {
|
||||
if (options.isNestedAttribute) {
|
||||
delete json?.attributes?.[key];
|
||||
} else if (options.isNestedSecret && json?.secrets?.[key]) {
|
||||
delete json?.secrets?.[key];
|
||||
if (Object.keys(json.secrets).length === 0) {
|
||||
delete json.secrets;
|
||||
}
|
||||
} else {
|
||||
delete json[key];
|
||||
}
|
||||
};
|
||||
|
||||
// For plugins, the `for` option is an object with a `name` key
|
||||
// For non-plugins, the `for` option can be string or an array
|
||||
if (
|
||||
(isPlugin && !options.for?.name.includes(compositeType)) ||
|
||||
(!isPlugin &&
|
||||
!options.for?.includes(recordType) &&
|
||||
typeOf(options.for) === 'array') ||
|
||||
(!isPlugin &&
|
||||
options.for !== recordType &&
|
||||
typeOf(options.for) === 'string')
|
||||
) {
|
||||
deleteKey(json);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
@@ -211,12 +244,10 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
* @return {object}
|
||||
*/
|
||||
normalizeMissingArrays(store, primaryModelClass, payload) {
|
||||
const attrDefs = store
|
||||
.getSchemaDefinitionService()
|
||||
.attributesDefinitionFor({ type: primaryModelClass.modelName });
|
||||
const attrDefs = store.schema.fields({ type: primaryModelClass.modelName });
|
||||
if (attrDefs) {
|
||||
Object.keys(attrDefs).forEach((key) => {
|
||||
if (!payload[key] && attrDefs[key]?.options?.emptyArrayIfMissing) {
|
||||
attrDefs.keys().forEach((key) => {
|
||||
if (!payload[key] && attrDefs.get(key)?.options?.emptyArrayIfMissing) {
|
||||
payload[key] = [];
|
||||
}
|
||||
});
|
||||
@@ -235,12 +266,10 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
* @return {object}
|
||||
*/
|
||||
normalizeMissingObjects(store, primaryModelClass, payload) {
|
||||
const attrDefs = store
|
||||
.getSchemaDefinitionService()
|
||||
.attributesDefinitionFor({ type: primaryModelClass.modelName });
|
||||
const attrDefs = store.schema.fields({ type: primaryModelClass.modelName });
|
||||
if (attrDefs) {
|
||||
Object.keys(attrDefs).forEach((key) => {
|
||||
if (!payload[key] && attrDefs[key]?.options?.emptyObjectIfMissing) {
|
||||
attrDefs.keys().forEach((key) => {
|
||||
if (!payload[key] && attrDefs.get(key)?.options?.emptyObjectIfMissing) {
|
||||
payload[key] = {};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import ApplicationSerializer from './application';
|
||||
import {
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
|
||||
options,
|
||||
} from '../models/credential-library';
|
||||
|
||||
@@ -28,6 +29,9 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
|
||||
case TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC:
|
||||
return this.serializeVaultGeneric(...arguments);
|
||||
case TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE:
|
||||
return this.serializeVaultSshCertificate(...arguments);
|
||||
case TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP:
|
||||
return this.serializeVaultLdap(...arguments);
|
||||
default:
|
||||
return super.serialize(...arguments);
|
||||
}
|
||||
@@ -43,22 +47,52 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
|
||||
if (!serialized.attributes?.http_method?.match(/post/i)) {
|
||||
serialized.attributes.http_request_body = null;
|
||||
}
|
||||
const { credential_type, credential_mapping_overrides } = serialized;
|
||||
this.handleCredentialMappingOverrides(serialized, isNew);
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
// API expects to send null to fields if it is undefined or deleted
|
||||
if (credential_mapping_overrides && !isNew) {
|
||||
serialized.credential_mapping_overrides = options.mapping_overrides[
|
||||
credential_type
|
||||
].reduce((obj, key) => {
|
||||
if (credential_mapping_overrides[key]) {
|
||||
obj[key] = credential_mapping_overrides[key];
|
||||
} else {
|
||||
obj[key] = null;
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
serializeVaultSshCertificate() {
|
||||
const serialized = super.serialize(...arguments);
|
||||
if (serialized.attributes) {
|
||||
const { key_type } = serialized.attributes;
|
||||
if (key_type !== 'rsa' && key_type !== 'ecdsa') {
|
||||
// If the key type is not RSA or ECDSA, set key_bits to null
|
||||
serialized.attributes.key_bits = null;
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
serializeVaultLdap(snapshot) {
|
||||
const { isNew } = snapshot;
|
||||
const serialized = super.serialize(...arguments);
|
||||
if (serialized.attributes) {
|
||||
this.handleCredentialMappingOverrides(serialized, isNew);
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle credential mapping overrides for serialization.
|
||||
* @param {Object} serialized - The serialized object.
|
||||
* @param {boolean} isNew - Indicates if the record is new.
|
||||
*/
|
||||
handleCredentialMappingOverrides(serialized, isNew) {
|
||||
const { credential_type, credential_mapping_overrides } = serialized;
|
||||
|
||||
// API expects to send null to fields if it is undefined or deleted
|
||||
if (credential_mapping_overrides && !isNew) {
|
||||
serialized.credential_mapping_overrides = options.mapping_overrides[
|
||||
credential_type
|
||||
].reduce((obj, key) => {
|
||||
if (credential_mapping_overrides[key]) {
|
||||
obj[key] = credential_mapping_overrides[key];
|
||||
} else {
|
||||
obj[key] = null;
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,101 +4,45 @@
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
import {
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
} from '../models/host-catalog';
|
||||
|
||||
// These fields are Azure specific
|
||||
const azureFields = [
|
||||
'disable_credential_rotation',
|
||||
'tenant_id',
|
||||
'client_id',
|
||||
'subscription_id',
|
||||
'secret_id',
|
||||
'secret_value',
|
||||
];
|
||||
|
||||
// These fields are AWS specific, AWS provider now has two credentialTypes: static and dynamic [not to be confused with static and dynamic host-catalogs]
|
||||
const AWSfieldsWithCredentialType = {
|
||||
'static-credential': [
|
||||
'access_key_id',
|
||||
'secret_access_key',
|
||||
'region',
|
||||
'disable_credential_rotation',
|
||||
'worker_filter',
|
||||
],
|
||||
'dynamic-credential': [
|
||||
'role_arn',
|
||||
'role_external_id',
|
||||
'role_session_name',
|
||||
'role_tags',
|
||||
'region',
|
||||
'disable_credential_rotation',
|
||||
'worker_filter',
|
||||
],
|
||||
};
|
||||
import { TYPE_CREDENTIAL_DYNAMIC } from '../models/host-catalog';
|
||||
|
||||
export default class HostCatalogSerializer extends ApplicationSerializer {
|
||||
serialize(snapshot) {
|
||||
const { compositeType, credentialType } = snapshot.record;
|
||||
const { credentialType } = snapshot.record;
|
||||
const serialized = super.serialize(...arguments);
|
||||
|
||||
// This is a hack to remove the worker_filter field from the serialized data if the compositeType is not AWS
|
||||
// Ideally we should refactor the model to include the compositeType using `for` attribute
|
||||
if (compositeType !== TYPE_HOST_CATALOG_PLUGIN_AWS) {
|
||||
delete serialized.worker_filter;
|
||||
}
|
||||
|
||||
// By default, disable credential rotation for dynamic credentials
|
||||
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
|
||||
serialized.attributes.disable_credential_rotation = true;
|
||||
}
|
||||
|
||||
switch (compositeType) {
|
||||
case 'static':
|
||||
return this.serializeStatic(...arguments);
|
||||
default:
|
||||
return serialized;
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
|
||||
serializeAttribute(snapshot, json, key, attribute) {
|
||||
const value = super.serializeAttribute(...arguments);
|
||||
const { compositeType, credentialType, isPlugin } = snapshot.record;
|
||||
const { compositeType, credentialType } = snapshot.record;
|
||||
const { options } = attribute;
|
||||
const isSamePluginName = options?.for?.name?.includes(compositeType);
|
||||
|
||||
const fields =
|
||||
compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE
|
||||
? azureFields
|
||||
: AWSfieldsWithCredentialType[credentialType];
|
||||
|
||||
// Delete any nested attribute fields that don't belong to the record type
|
||||
if (isPlugin && options.isNestedAttribute && json.attributes) {
|
||||
if (!fields.includes(key)) {
|
||||
delete json.attributes[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any secret fields that don't belong to the record type
|
||||
if (isPlugin && options.isNestedSecret && json.secrets) {
|
||||
if (!fields.includes(key)) {
|
||||
// Remove attributes that are not applicable to the current credential type for the same plugin
|
||||
// For example, 'aws' dynamic plugin has two credential types: 'static' and 'dynamic'
|
||||
// API expects the fields to be 'null' for the fields that are not from the current credential type (that's why we are not using the application serializer for this, where we delete attrs)
|
||||
// Empty secrets should be removed from the JSON
|
||||
if (
|
||||
isSamePluginName &&
|
||||
options.for.credentialType &&
|
||||
options.for.credentialType !== credentialType
|
||||
) {
|
||||
if (options.isNestedAttribute) {
|
||||
json.attributes[key] = null;
|
||||
} else if (options.isNestedSecret && json.secrets?.[key]) {
|
||||
delete json.secrets[key];
|
||||
if (json['secrets'] && Object.keys(json.secrets).length === 0) {
|
||||
if (Object.keys(json.secrets).length === 0) {
|
||||
delete json.secrets;
|
||||
}
|
||||
} else {
|
||||
delete json[key];
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
serializeStatic() {
|
||||
const serialized = super.serialize(...arguments);
|
||||
// Delete unnecessary fields for static host-catalog
|
||||
delete serialized.attributes;
|
||||
delete serialized.secrets;
|
||||
delete serialized.worker_filter;
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,6 @@
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
const fieldByType = {
|
||||
aws: ['preferred_endpoints', 'filters', 'sync_interval_seconds'],
|
||||
azure: ['preferred_endpoints', 'filter_string', 'sync_interval_seconds'],
|
||||
};
|
||||
export default class HostSetSerializer extends ApplicationSerializer {
|
||||
// =properties
|
||||
|
||||
@@ -63,16 +59,8 @@ export default class HostSetSerializer extends ApplicationSerializer {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
serializeAttribute(snapshot, json, key, attribute) {
|
||||
serializeAttribute(snapshot, json, key) {
|
||||
const value = super.serializeAttribute(...arguments);
|
||||
const { isPlugin, compositeType } = snapshot.record;
|
||||
const { options } = attribute;
|
||||
|
||||
if (isPlugin && options.isNestedAttribute) {
|
||||
if (!fieldByType[compositeType].includes(key)) {
|
||||
delete json.attributes[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'filter_string') {
|
||||
const { filter_string } = json.attributes;
|
||||
@@ -107,4 +95,18 @@ export default class HostSetSerializer extends ApplicationSerializer {
|
||||
|
||||
return super.normalize(typeClass, normalizedHash, ...rest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts errors from the payload and transforms specific keys if needed.
|
||||
* @override
|
||||
* @return {object}
|
||||
*/
|
||||
extractErrors() {
|
||||
const errors = super.extractErrors(...arguments);
|
||||
if (errors?.filter) {
|
||||
errors.filter_string = errors.filter;
|
||||
delete errors.filter;
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -53,4 +53,18 @@ export default class ManagedGroupSerializer extends ApplicationSerializer {
|
||||
|
||||
return super.normalize(typeClass, normalizedHash, ...rest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts errors from the payload and transforms specific keys if needed.
|
||||
* @override
|
||||
* @return {object}
|
||||
*/
|
||||
extractErrors() {
|
||||
const errors = super.extractErrors(...arguments);
|
||||
if (errors?.filter) {
|
||||
errors.filter_string = errors.filter;
|
||||
delete errors.filter;
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ export default class StorageBucketSerializer extends ApplicationSerializer {
|
||||
const value = super.serializeAttribute(...arguments);
|
||||
const { credentialType } = snapshot.record;
|
||||
const { options } = attribute;
|
||||
|
||||
// This deletes any fields that don't belong to the record's credential type
|
||||
if (options.isNestedAttribute && json.attributes) {
|
||||
// The key must be included in the fieldsByType list above
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Service from '@ember/service';
|
||||
import Dexie from 'dexie';
|
||||
import { typeOf } from '@ember/utils';
|
||||
|
||||
// List of resources we support to be cached and searched in
|
||||
// indexedDb. Any field that can be searched upon should be added to
|
||||
// this index. Increment the version number of the database whenever
|
||||
// indexes change.
|
||||
export const modelIndexes = {
|
||||
token: '&id, token',
|
||||
target:
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.address, attributes.scope.scope_id',
|
||||
session:
|
||||
'&id, attributes.created_time, attributes.type, attributes.status, attributes.endpoint, attributes.target_id, attributes.user_id, attributes.scope.scope_id',
|
||||
user: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
|
||||
group:
|
||||
'&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
|
||||
role: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
|
||||
scope:
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id',
|
||||
'credential-store':
|
||||
'&id, attributes.created_time, attributes.name, attributes.description, attributes.type, attributes.scope.scope_id',
|
||||
'host-catalog':
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id, attributes.plugin.name',
|
||||
'auth-method':
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.is_primary, attributes.scope.scope_id',
|
||||
'session-recording':
|
||||
'&id, attributes.created_time, attributes.type, attributes.state, attributes.start_time, attributes.end_time, attributes.duration, attributes.scope.scope_id, attributes.create_time_values.user.id, attributes.create_time_values.user.name, attributes.create_time_values.target.id, attributes.create_time_values.target.name, attributes.create_time_values.target.scope.id, attributes.create_time_values.target.scope.name',
|
||||
alias:
|
||||
'&id, attributes.created_time, attributes.type, attributes.value, attributes.name, attributes.description, attributes.destination_id, attributes.scope.scope_id',
|
||||
};
|
||||
|
||||
export const formatDbName = (userId, clusterUrl) =>
|
||||
`boundary-${userId}-${clusterUrl}`;
|
||||
|
||||
/**
|
||||
* Recursively copy over the object and convert any booleans to "true" or "false" so they can be indexed.
|
||||
* Also converts any ember-like arrays back to a standard array.
|
||||
* @param result
|
||||
* @param object
|
||||
* @returns {*}
|
||||
*/
|
||||
const convertFields = (result, object) => {
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
if (typeOf(value) === 'object') {
|
||||
result[key] = convertFields({}, value);
|
||||
} else if (typeOf(value) === 'boolean') {
|
||||
result[key] = value ? 'true' : 'false';
|
||||
} else if (typeOf(value) === 'array') {
|
||||
result[key] = [...value];
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively copy over the object and convert any string "true" or "false" back to booleans.
|
||||
* Will also convert any array fields back using their model transformation.
|
||||
* @param result
|
||||
* @param object
|
||||
* @param schema
|
||||
* @param serializer
|
||||
* @returns {*}
|
||||
*/
|
||||
const unconvertFields = (result, object, schema, serializer) => {
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
if (typeOf(value) === 'object') {
|
||||
result[key] = unconvertFields({}, value, schema, serializer);
|
||||
} else if (value === 'true' || value === 'false') {
|
||||
result[key] = value === 'true';
|
||||
} else if (typeOf(value) === 'array') {
|
||||
const attribute = schema.attributes.get(key);
|
||||
|
||||
// If attribute type is array, we transform it back to a tracked array
|
||||
if (attribute?.type === 'array') {
|
||||
const transformer = serializer.transformFor(attribute.type);
|
||||
result[key] = transformer.deserialize(value, attribute.options);
|
||||
return;
|
||||
}
|
||||
|
||||
result[key] = value;
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Service to encapsulate the IndexedDB implementation. To use this service, call
|
||||
* `setup` from the application root and
|
||||
*/
|
||||
export default class IndexedDbService extends Service {
|
||||
// =attributes
|
||||
#db;
|
||||
get db() {
|
||||
return this.#db;
|
||||
}
|
||||
|
||||
setup(dbName) {
|
||||
// Don't run setup again if we already have one or if we didn't get a name
|
||||
if (this.#db || !dbName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#db = new Dexie(dbName);
|
||||
this.#db.version(2).stores(modelIndexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup the data by converting all fields to plain javascript objects.
|
||||
* We will also convert booleans to "false" or "true".
|
||||
* IndexedDB doesn't support indexes on boolean values so we have to
|
||||
* convert them to be able to support using boolean values.
|
||||
*
|
||||
* Undo the operation by setting `convertFields` to false.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {Boolean} cleanData
|
||||
* @param {Object} schema
|
||||
* @param {Object} serializer
|
||||
* @return {{id, type, attributes: {}, relationships: {}}}
|
||||
*/
|
||||
normalizeData({ data, cleanData, schema, serializer }) {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
let { id, type, attributes = {}, relationships = {} } = data;
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
attributes: cleanData
|
||||
? convertFields({}, attributes)
|
||||
: unconvertFields({}, attributes, schema, serializer),
|
||||
relationships: cleanData
|
||||
? convertFields({}, relationships)
|
||||
: unconvertFields({}, relationships, schema, serializer),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import Service from '@ember/service';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* Takes a POJO representing a query and converts it to a boolean expression
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Service from '@ember/service';
|
||||
import { PWBHost } from 'promise-worker-bi';
|
||||
|
||||
export const formatDbName = (userId, clusterUrl) =>
|
||||
`boundary-${userId}-${clusterUrl}`;
|
||||
|
||||
// A mapping of columns to their expected model attributes for supported models
|
||||
// The JSON data column is not included here but is assumed to always be last.
|
||||
export const modelMapping = {
|
||||
target: {
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
address: 'address',
|
||||
scope_id: 'scope.scope_id',
|
||||
created_time: 'created_time',
|
||||
},
|
||||
alias: {
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
destination_id: 'destination_id',
|
||||
value: 'value',
|
||||
scope_id: 'scope.scope_id',
|
||||
created_time: 'created_time',
|
||||
},
|
||||
group: {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
scope_id: 'scope.scope_id',
|
||||
created_time: 'created_time',
|
||||
},
|
||||
role: {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
scope_id: 'scope.scope_id',
|
||||
created_time: 'created_time',
|
||||
},
|
||||
user: {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
scope_id: 'scope.scope_id',
|
||||
created_time: 'created_time',
|
||||
},
|
||||
'credential-store': {
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
scope_id: 'scope.scope_id',
|
||||
created_time: 'created_time',
|
||||
},
|
||||
scope: {
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
scope_id: 'scope.scope_id',
|
||||
created_time: 'created_time',
|
||||
},
|
||||
'auth-method': {
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
is_primary: 'is_primary',
|
||||
scope_id: 'scope.scope_id',
|
||||
created_time: 'created_time',
|
||||
},
|
||||
'host-catalog': {
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
plugin_name: 'plugin.name',
|
||||
scope_id: 'scope.scope_id',
|
||||
created_time: 'created_time',
|
||||
},
|
||||
'session-recording': {
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
state: 'state',
|
||||
start_time: 'start_time',
|
||||
end_time: 'end_time',
|
||||
duration: 'duration',
|
||||
scope_id: 'scope.scope_id',
|
||||
user_id: 'create_time_values.user.id',
|
||||
user_name: 'create_time_values.user.name',
|
||||
target_id: 'create_time_values.target.id',
|
||||
target_name: 'create_time_values.target.name',
|
||||
target_scope_id: 'create_time_values.target.scope.id',
|
||||
target_scope_name: 'create_time_values.target.scope.name',
|
||||
created_time: 'created_time',
|
||||
},
|
||||
session: {
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
status: 'status',
|
||||
endpoint: 'endpoint',
|
||||
target_id: 'target_id',
|
||||
user_id: 'user_id',
|
||||
scope_id: 'scope.scope_id',
|
||||
created_time: 'created_time',
|
||||
},
|
||||
};
|
||||
|
||||
// A list of tables that we support searching using FTS5 in SQLite.
|
||||
export const searchTables = new Set([
|
||||
'target',
|
||||
'alias',
|
||||
'group',
|
||||
'role',
|
||||
'user',
|
||||
'credential-store',
|
||||
'scope',
|
||||
'auth-method',
|
||||
'host-catalog',
|
||||
'session-recording',
|
||||
'session',
|
||||
]);
|
||||
|
||||
export default class SqliteDbService extends Service {
|
||||
// =attributes
|
||||
|
||||
// We need a handle on the original web worker to properly clean it up.
|
||||
webWorker;
|
||||
// This will be the worker we interact with that is wrapped by PWBHost.
|
||||
worker;
|
||||
|
||||
setup(dbName) {
|
||||
if (this.worker) {
|
||||
// If the worker already exists, we don't need to do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
// In production, our JS files will be fingerprinted with an MD5 hash
|
||||
// so we need a way to map the worker file name to be able to find it.
|
||||
//
|
||||
// Ember is automagically replacing any URL path with the fingerprinted
|
||||
// asset, and this is done by broccoli-asset-rev.
|
||||
//
|
||||
// The problem is it's a bit finicky as seen in this issue
|
||||
// https://github.com/ember-cli/broccoli-asset-rewrite/issues/66
|
||||
// and will not work properly if it's part of a template string or interpolated.
|
||||
// Therefore, we need the URL path in a string somewhere to be replaced properly.
|
||||
this.webWorker = new Worker('/workers/sqlite-worker.js', {
|
||||
type: 'module',
|
||||
});
|
||||
|
||||
// We use PWBHost to simplify communication so we can just send and receive
|
||||
// messages with promises rather than having to manually set up event listeners
|
||||
this.worker = new PWBHost(this.webWorker);
|
||||
this.worker.register(async ({ method }) => {
|
||||
if (method === 'getDatabaseName') {
|
||||
return dbName;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchResource({ sql, parameters }) {
|
||||
return this.worker.postMessage({
|
||||
method: 'fetchResource',
|
||||
payload: { sql, parameters },
|
||||
});
|
||||
}
|
||||
|
||||
insertResource(resource, items) {
|
||||
return this.worker.postMessage({
|
||||
method: 'insertResource',
|
||||
payload: { resource, items, modelMapping },
|
||||
});
|
||||
}
|
||||
|
||||
deleteResource(resource, ids) {
|
||||
return this.worker.postMessage({
|
||||
method: 'deleteResource',
|
||||
payload: { resource, ids },
|
||||
});
|
||||
}
|
||||
|
||||
analyzeDatabase() {
|
||||
return this.worker.postMessage({ method: 'analyzeDatabase' });
|
||||
}
|
||||
|
||||
async downloadDatabase() {
|
||||
const byteArray = await this.worker.postMessage({
|
||||
method: 'downloadDatabase',
|
||||
});
|
||||
|
||||
const blob = new Blob([byteArray.buffer], {
|
||||
type: 'application/x-sqlite3',
|
||||
});
|
||||
|
||||
// Create temporary elements to initialize the download
|
||||
const a = window.document.createElement('a');
|
||||
window.document.body.appendChild(a);
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
a.download = 'boundary.db';
|
||||
a.addEventListener('click', function () {
|
||||
setTimeout(function () {
|
||||
window.URL.revokeObjectURL(a.href);
|
||||
a.remove();
|
||||
}, 500);
|
||||
});
|
||||
a.click();
|
||||
}
|
||||
|
||||
async clearDatabase() {
|
||||
await this.worker.postMessage({ method: 'clearDatabase' });
|
||||
}
|
||||
|
||||
async deleteDatabase() {
|
||||
await this.worker.postMessage({ method: 'deleteDatabase' });
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
this.webWorker?.terminate();
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class MemoryStorage {
|
||||
*
|
||||
* @example
|
||||
* import Route from '@ember/routing/route';
|
||||
* import { inject as service } from '@ember/service';
|
||||
* import { service } from '@ember/service';
|
||||
* export default class ApplicationRoute extends Route {
|
||||
* @service storage;
|
||||
* afterModel() {
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Store, { CacheHandler } from '@ember-data/store';
|
||||
// Importing 'Store' from 'ember-data/store' to get default configuration automatically.
|
||||
// TODO: Remove eslint-disable after upgrading to eslint-plugin-ember >= v12.2.1
|
||||
// eslint-disable-next-line ember/use-ember-data-rfc-395-imports
|
||||
import Store from 'ember-data/store';
|
||||
import { CacheHandler } from '@ember-data/store';
|
||||
import RequestManager from '@ember-data/request';
|
||||
import { LegacyNetworkHandler } from '@ember-data/legacy-compat';
|
||||
import IndexedDbHandler from 'api/handlers/indexed-db-handler';
|
||||
import SqliteHandler from 'api/handlers/sqlite-handler';
|
||||
|
||||
export default class extends Store {
|
||||
requestManager = new RequestManager();
|
||||
@@ -14,9 +18,9 @@ export default class extends Store {
|
||||
constructor(args) {
|
||||
super(args);
|
||||
|
||||
const indexedDbHandler = new IndexedDbHandler(this);
|
||||
const sqliteHandler = new SqliteHandler(this);
|
||||
|
||||
this.requestManager.use([indexedDbHandler, LegacyNetworkHandler]);
|
||||
this.requestManager.use([sqliteHandler, LegacyNetworkHandler]);
|
||||
this.requestManager.useCache(CacheHandler);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class AccountValueMapArrayTransform extends Transform {
|
||||
export default class AccountValueMapArrayTransform {
|
||||
deserialize(serialized) {
|
||||
const accountValues = serialized || [];
|
||||
return accountValues.map((accountValueString) => ({
|
||||
@@ -18,4 +16,8 @@ export default class AccountValueMapArrayTransform extends Transform {
|
||||
const accountValues = deserialized || [];
|
||||
return accountValues.map(({ key, value }) => `${key}=${value}`);
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,9 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
import { TrackedArray } from 'tracked-built-ins';
|
||||
|
||||
export default class ArrayTransform extends Transform {
|
||||
export default class ArrayTransform {
|
||||
deserialize(serialized) {
|
||||
const arr = new TrackedArray(serialized || []);
|
||||
return arr;
|
||||
@@ -16,4 +15,8 @@ export default class ArrayTransform extends Transform {
|
||||
const arr = deserialized || [];
|
||||
return arr;
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class DurationTransform extends Transform {
|
||||
export default class DurationTransform {
|
||||
/**
|
||||
* Normalize a duration string in the format "3.0001s" to milliseconds.
|
||||
* The backend returns a precision of nanoseconds but we round to whole
|
||||
@@ -37,4 +35,8 @@ export default class DurationTransform extends Transform {
|
||||
maximumFractionDigits: 9,
|
||||
})}s`;
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class HostSourceIdArrayTransform extends Transform {
|
||||
export default class HostSourceIdArrayTransform {
|
||||
deserialize(serialized) {
|
||||
const stringValues = serialized || [];
|
||||
return stringValues.map(({ id: host_source_id, ...obj }) => ({
|
||||
@@ -18,4 +16,8 @@ export default class HostSourceIdArrayTransform extends Transform {
|
||||
const strings = deserialized || [];
|
||||
return strings.map(({ host_source_id: id, ...obj }) => ({ id, ...obj }));
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,12 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class ObjectAsArrayTransform extends Transform {
|
||||
export default class ObjectAsArrayTransform {
|
||||
deserialize(serialized) {
|
||||
const obj = serialized || {};
|
||||
return Object.entries(obj).map(([key, value]) => ({ key, value }));
|
||||
}
|
||||
|
||||
serialize(deserialized) {
|
||||
const array = deserialized || [];
|
||||
|
||||
@@ -24,4 +23,8 @@ export default class ObjectAsArrayTransform extends Transform {
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,9 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
import { TrackedObject } from 'tracked-built-ins';
|
||||
|
||||
export default class ObjectTransform extends Transform {
|
||||
export default class ObjectTransform {
|
||||
/**
|
||||
* @param serialized
|
||||
* @returns {object}
|
||||
@@ -26,4 +25,8 @@ export default class ObjectTransform extends Transform {
|
||||
}
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class StringArrayTransform extends Transform {
|
||||
export default class StringArrayTransform {
|
||||
deserialize(serialized) {
|
||||
const stringValues = serialized || [];
|
||||
return stringValues.map((value) => ({ value }));
|
||||
@@ -15,4 +13,8 @@ export default class StringArrayTransform extends Transform {
|
||||
const strings = deserialized || [];
|
||||
return strings.map(({ value }) => value);
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
import { typeOf } from '@ember/utils';
|
||||
|
||||
/**
|
||||
* Flattens a nested object into a single-level object.
|
||||
* @param obj
|
||||
* @param result
|
||||
* @param parentKey
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
export const flattenObject = (obj, result = {}, parentKey = '') => {
|
||||
for (const key in obj) {
|
||||
// Check if the value is an object and it is not null
|
||||
if (typeOf(obj[key]) === 'object' && obj[key] !== null) {
|
||||
// Recursively flatten the object
|
||||
flattenObject(obj[key], result, `${parentKey}${key}.`);
|
||||
} else if (obj[key] !== null && obj[key] !== '') {
|
||||
// Only add to result if the value is not null or an empty string
|
||||
result[`${parentKey}${key}`] = obj[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -1,263 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { get } from '@ember/object';
|
||||
import { modelIndexes } from '../services/indexed-db';
|
||||
|
||||
/**
|
||||
* Takes a POJO representing a filter query and builds a query to
|
||||
* query indexedDb with. Note that the first filter is important as that
|
||||
* will use the actual index in indexedDb while any subsequent ones will be less efficient.
|
||||
*
|
||||
* @example
|
||||
* const filter = queryIndexedDb(indexedDb, type, {
|
||||
* search: generated',
|
||||
* filters: {
|
||||
* scope_id: {
|
||||
* logicalOperator: 'and',
|
||||
* values: [{ notEquals: 'scope1' }, { notEquals: 'scope2' }],
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export async function queryIndexedDb(indexedDb, resource, query) {
|
||||
let { search, filters = {} } = query ?? {};
|
||||
let filterCollection = indexedDb[resource];
|
||||
|
||||
search = getSearchObject(search, resource);
|
||||
filters = cleanObject(filters);
|
||||
|
||||
if (Object.keys(filters).length) {
|
||||
// Remove the first item to use as the initial collection and then reduce the remaining ones
|
||||
const entries = Object.entries(filters);
|
||||
const [firstKey, firstFilterArrayOrObject] = entries.shift();
|
||||
|
||||
filterCollection = await entries.reduce(
|
||||
async (currCollection, [key, filterArrayOrObject]) => {
|
||||
// Grab the results from the set of one filters to be `AND` together with the rest of the filters
|
||||
const filterResults = await buildInitialWhereClause({
|
||||
table: indexedDb[resource],
|
||||
key,
|
||||
filterArrayOrObject,
|
||||
}).primaryKeys();
|
||||
const resultIdsSet = new Set(filterResults);
|
||||
|
||||
return (await currCollection).and((record) => {
|
||||
return resultIdsSet.has(record.id);
|
||||
});
|
||||
},
|
||||
buildInitialWhereClause({
|
||||
key: firstKey,
|
||||
table: indexedDb[resource],
|
||||
filterArrayOrObject: firstFilterArrayOrObject,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const { text, fields } = search;
|
||||
|
||||
filterCollection = filterCollection.filter((record) =>
|
||||
fields
|
||||
.map((key) => get(record, key))
|
||||
.some((element) => {
|
||||
if (typeof element === 'string') {
|
||||
return element?.toLowerCase().includes(text?.toLowerCase());
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// OrderBy has to be used on where clauses while if it's a
|
||||
// collection already we have to use sortBy. We'll also sort by
|
||||
// descending order based on created time to match the API as any
|
||||
// "or" clause can change the order
|
||||
if (filterCollection.orderBy) {
|
||||
return filterCollection.orderBy(getKey('created_time')).reverse().toArray();
|
||||
}
|
||||
return filterCollection.reverse().sortBy(getKey('created_time'));
|
||||
}
|
||||
|
||||
const buildInitialWhereClause = ({ filterArrayOrObject, table, key }) => {
|
||||
const filterValueArray = Array.isArray(filterArrayOrObject)
|
||||
? filterArrayOrObject
|
||||
: filterArrayOrObject.values;
|
||||
|
||||
const { logicalOperator } = filterArrayOrObject;
|
||||
|
||||
// Short circuit if all the filter types were equals or notEquals since indexedDb
|
||||
// can more efficiently query if we pass all values at once
|
||||
const checkAllFilterType = (type) =>
|
||||
filterValueArray.every((obj) => Object.keys(obj)[0] === type);
|
||||
if (checkAllFilterType('equals')) {
|
||||
return table
|
||||
.where(getKey(key))
|
||||
.anyOf(filterValueArray.map((obj) => obj['equals']));
|
||||
}
|
||||
if (checkAllFilterType('notEquals')) {
|
||||
return table
|
||||
.where(getKey(key))
|
||||
.noneOf(filterValueArray.map((obj) => obj['notEquals']));
|
||||
}
|
||||
|
||||
// Remove the first item to use as the initial where clause and then reduce the remaining ones
|
||||
const [firstOperation, firstFilterValue] = Object.entries(
|
||||
filterValueArray[0],
|
||||
)[0];
|
||||
|
||||
// Not all are filters are equals or notEquals,
|
||||
// manually build the initial where clause and resulting collections
|
||||
return filterValueArray.slice(1).reduce(
|
||||
(result, currObj) => {
|
||||
const [operation, value] = Object.entries(currObj)[0];
|
||||
|
||||
return buildIndexedDbCollection({
|
||||
key: getKey(key),
|
||||
value,
|
||||
collection: result,
|
||||
operation,
|
||||
logicalOperator,
|
||||
});
|
||||
},
|
||||
buildIndexedDbWhere({
|
||||
table,
|
||||
key: getKey(key),
|
||||
operation: firstOperation,
|
||||
value: firstFilterValue,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the initial where clause for indexed DB to filter on. Note that this makes
|
||||
* the first filter important as that will do the real filtering from indexed DB while
|
||||
* any subsequent filter has to be done in memory.
|
||||
*/
|
||||
const buildIndexedDbWhere = ({ table, key, value, operation }) => {
|
||||
const baseWhereClause = table.where(key);
|
||||
switch (operation) {
|
||||
case 'contains':
|
||||
// We return an error due to not being able to support contains on first filter due to performance.
|
||||
throw new Error('Contains is not supported as the first filter.');
|
||||
case 'gt':
|
||||
return baseWhereClause.above(value);
|
||||
case 'gte':
|
||||
return baseWhereClause.aboveOrEqual(value);
|
||||
case 'lt':
|
||||
return baseWhereClause.below(value);
|
||||
case 'lte':
|
||||
return baseWhereClause.belowOrEqual(value);
|
||||
case 'notEquals':
|
||||
return baseWhereClause.notEqual(value);
|
||||
default:
|
||||
return baseWhereClause.equals(value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Continues building the queries for indexed DB to filter on. Note that these are not true
|
||||
* indexed DB queries and in fact just standard JS filters for logical "and" queries and parallel queries
|
||||
* to indexed DB for logical "or" queries that get deduped.
|
||||
*/
|
||||
const buildIndexedDbCollection = ({
|
||||
key,
|
||||
value,
|
||||
collection,
|
||||
operation,
|
||||
logicalOperator,
|
||||
}) => {
|
||||
if (logicalOperator === 'and') {
|
||||
switch (operation) {
|
||||
case 'contains':
|
||||
return collection.and((record) => get(record, key)?.includes(value));
|
||||
case 'gt':
|
||||
return collection.and((record) => get(record, key) > value);
|
||||
case 'gte':
|
||||
return collection.and((record) => get(record, key) >= value);
|
||||
case 'lt':
|
||||
return collection.and((record) => get(record, key) < value);
|
||||
case 'lte':
|
||||
return collection.and((record) => get(record, key) <= value);
|
||||
case 'notEquals':
|
||||
return collection.and((record) => get(record, key) !== value);
|
||||
default:
|
||||
return collection.and((record) => get(record, key) === value);
|
||||
}
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
case 'contains':
|
||||
// We return an error due to not being able to support contains on this filter due to performance.
|
||||
throw new Error(
|
||||
'Contains is not supported as a filter option with "or" operator.',
|
||||
);
|
||||
case 'gt':
|
||||
return collection.or(key).above(value);
|
||||
case 'gte':
|
||||
return collection.or(key).aboveOrEqual(value);
|
||||
case 'lt':
|
||||
return collection.or(key).below(value);
|
||||
case 'lte':
|
||||
return collection.or(key).belowOrEqual(value);
|
||||
case 'notEquals':
|
||||
return collection.or(key).notEqual(value);
|
||||
default:
|
||||
return collection.or(key).equals(value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates the fields passed in from the UI for resources to the keys indexed DB knows them as.
|
||||
* Usually it's the path of the actual normalized model.
|
||||
*/
|
||||
const getKey = (key) => {
|
||||
switch (key) {
|
||||
case 'scope_id':
|
||||
return `attributes.scope.${key}`;
|
||||
case 'id':
|
||||
return key;
|
||||
default:
|
||||
return `attributes.${key}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleans the object to remove any keys that have a value of an empty array. This makes
|
||||
* it easier so we don't have to worry about empty arrays getting queried in indexed db.
|
||||
*/
|
||||
const cleanObject = (obj) => {
|
||||
if (!obj) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.entries(obj).reduce((result, [key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
result[key] = value;
|
||||
}
|
||||
} else if (value.values.length > 0) {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the search object that is expected when a user searches. Uses
|
||||
* the indexes we setup in indexed db.
|
||||
*/
|
||||
const getSearchObject = (text, resource) => {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
fields: modelIndexes[resource]
|
||||
.split(', ')
|
||||
.map((index) => index.replace('&', '')),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { modelMapping } from 'api/services/sqlite';
|
||||
import { searchTables } from 'api/services/sqlite';
|
||||
import { typeOf } from '@ember/utils';
|
||||
import { underscore } from '@ember/string';
|
||||
|
||||
/**
|
||||
* Takes a POJO representing a filter query and builds a SQL query.
|
||||
*
|
||||
* @example
|
||||
* const filter = generateSQLExpressions(type, {
|
||||
* search: generated',
|
||||
* filters: {
|
||||
* scope_id: {
|
||||
* logicalOperator: 'and',
|
||||
* values: [{ notEquals: 'scope1' }, { notEquals: 'scope2' }],
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function generateSQLExpressions(
|
||||
resource,
|
||||
query = {},
|
||||
{ page, pageSize, select } = {},
|
||||
) {
|
||||
let { search, filters, sort = {} } = query;
|
||||
const conditions = [];
|
||||
const parameters = [];
|
||||
const tableName = underscore(resource);
|
||||
|
||||
addFilterConditions({ filters, parameters, conditions });
|
||||
addSearchConditions({ search, resource, tableName, parameters, conditions });
|
||||
|
||||
const orderByClause = constructOrderByClause(resource, sort);
|
||||
|
||||
const whereClause = conditions.length ? `WHERE ${and(conditions)}` : '';
|
||||
|
||||
const paginationClause = page && pageSize ? `LIMIT ? OFFSET ?` : '';
|
||||
if (paginationClause) {
|
||||
parameters.push(pageSize, (page - 1) * pageSize);
|
||||
}
|
||||
|
||||
const selectClause = `SELECT ${select ? select.join(', ') : '*'} FROM "${tableName}"`;
|
||||
|
||||
return {
|
||||
// Replace any empty newlines or leading whitespace on each line to be consistent with formatting
|
||||
// This is mainly to help us read and test the generated SQL as it has no effect on the actual SQL execution.
|
||||
sql: `
|
||||
${selectClause}
|
||||
${whereClause}
|
||||
${orderByClause}
|
||||
${paginationClause}`
|
||||
.replace(/^\s+/gm, '')
|
||||
.trim(),
|
||||
parameters,
|
||||
};
|
||||
}
|
||||
|
||||
function addFilterConditions({ filters, parameters, conditions }) {
|
||||
if (!filters) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, filterArrayOrObject] of Object.entries(filters)) {
|
||||
// Filter can be an array or an object with a `values` property
|
||||
const filterValueArray = Array.isArray(filterArrayOrObject)
|
||||
? filterArrayOrObject
|
||||
: filterArrayOrObject.values;
|
||||
|
||||
if (!filterValueArray || !filterValueArray.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// When attribute conditions are a series of equals/notEquals
|
||||
// replace with in/notIn to avoid hitting the sqlite maximum
|
||||
// expression tree depth.
|
||||
const firstOperator = Object.keys(filterValueArray[0])[0];
|
||||
const allOperatorsEqual = filterValueArray
|
||||
.flatMap((item) => Object.keys(item))
|
||||
.every((op) => op === firstOperator);
|
||||
if (
|
||||
filterValueArray.length > 1 &&
|
||||
(firstOperator === 'equals' || firstOperator === 'notEquals') &&
|
||||
allOperatorsEqual
|
||||
) {
|
||||
const operation = firstOperator === 'equals' ? 'in' : 'notIn';
|
||||
const values = filterValueArray
|
||||
.filter((f) => f)
|
||||
.map((filterObjValue) => {
|
||||
let value = Object.values(filterObjValue)[0];
|
||||
if (typeOf(value) === 'date') {
|
||||
value = value.toISOString();
|
||||
}
|
||||
parameters.push(value);
|
||||
return value;
|
||||
});
|
||||
const filterCondition = `${key}${OPERATORS[operation](values)}`;
|
||||
conditions.push(parenthetical(filterCondition));
|
||||
continue;
|
||||
}
|
||||
|
||||
const filterConditions = filterValueArray
|
||||
.filter((f) => f)
|
||||
.map((filterObjValue) => {
|
||||
let [operation, value] = Object.entries(filterObjValue)[0];
|
||||
|
||||
// SQLite needs to be working with ISO strings
|
||||
if (typeOf(value) === 'date') {
|
||||
value = value.toISOString();
|
||||
}
|
||||
|
||||
// Handle LIKE operator separately
|
||||
if (operation === 'contains') {
|
||||
parameters.push(`%${value}%`);
|
||||
} else {
|
||||
parameters.push(value);
|
||||
}
|
||||
|
||||
return `${key}${OPERATORS[operation]}`;
|
||||
});
|
||||
|
||||
const { logicalOperator } = filterArrayOrObject;
|
||||
// Default to `or` if we don't have a logical operator
|
||||
conditions.push(
|
||||
parenthetical(
|
||||
logicalOperator === 'and'
|
||||
? and(filterConditions)
|
||||
: or(filterConditions),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function addSearchConditions({
|
||||
search,
|
||||
resource,
|
||||
tableName,
|
||||
parameters,
|
||||
conditions,
|
||||
}) {
|
||||
if (!search) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchTables.has(resource)) {
|
||||
// Use the special prefix indicator "*" for full-text search
|
||||
parameters.push(`"${search}"*`);
|
||||
// Use a subquery to match against the FTS table with rowids as SQLite is
|
||||
// much more efficient with FTS queries when using rowids or MATCH (or both).
|
||||
// We could have also used a join here but a subquery is simpler.
|
||||
conditions.push(
|
||||
`rowid IN (SELECT rowid FROM ${tableName}_fts WHERE ${tableName}_fts MATCH ?)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = Object.keys(modelMapping[resource]);
|
||||
const searchConditions = parenthetical(
|
||||
or(
|
||||
fields.map((field) => {
|
||||
parameters.push(`%${search}%`);
|
||||
return `${field}${OPERATORS['contains']}`;
|
||||
}),
|
||||
),
|
||||
);
|
||||
conditions.push(searchConditions);
|
||||
}
|
||||
|
||||
function constructOrderByClause(resource, sort) {
|
||||
const defaultOrderByClause = 'ORDER BY created_time DESC';
|
||||
|
||||
const { attributes, customSort, direction, isCoalesced } = sort;
|
||||
const sortDirection = direction === 'desc' ? 'DESC' : 'ASC';
|
||||
|
||||
// We have to check if the attributes are valid for the resource
|
||||
// as we can't use parameterized queries for ORDER BY
|
||||
const validAttributes = Object.keys(modelMapping[resource] ?? {});
|
||||
if (attributes?.some((attr) => !validAttributes.includes(attr))) {
|
||||
return modelMapping[resource]?.created_time ? defaultOrderByClause : '';
|
||||
}
|
||||
|
||||
if (customSort?.attributeMap) {
|
||||
const whenClauses = Object.keys(customSort.attributeMap).reduce(
|
||||
(acc, key) => {
|
||||
return acc + `WHEN '${key}' THEN '${customSort.attributeMap[key]}' `;
|
||||
},
|
||||
'',
|
||||
);
|
||||
return `ORDER BY CASE ${attributes.join(', ')} ${whenClauses}END ${sortDirection}`;
|
||||
} else if (attributes?.length > 0) {
|
||||
const commaSeparatedVals = attributes.join(', ');
|
||||
|
||||
// In places where `collate nocase` is used, it is to ensure case is ignored on the initial sort.
|
||||
// Then, a sort on the same condition is performed to ensure upper-case strings are given preference in a tie.
|
||||
if (isCoalesced) {
|
||||
return `ORDER BY COALESCE(${attributes.join(', ')}) COLLATE NOCASE ${sortDirection}, COALESCE(${commaSeparatedVals}) ${sortDirection}`;
|
||||
}
|
||||
|
||||
const attributesWithNoCollate = attributes
|
||||
.map((attr) => `${attr} COLLATE NOCASE ${sortDirection}`)
|
||||
.join(', ');
|
||||
const attributesWithDirection = attributes
|
||||
.map((attr) => `${attr} ${sortDirection}`)
|
||||
.join(', ');
|
||||
return `ORDER BY ${attributesWithNoCollate}, ${attributesWithDirection}`;
|
||||
} else if (modelMapping[resource]?.created_time) {
|
||||
return defaultOrderByClause;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Comparison Operators
|
||||
const OPERATORS = {
|
||||
equals: ' = ?',
|
||||
notEquals: ' != ?',
|
||||
gt: ' > ?',
|
||||
gte: ' >= ?',
|
||||
lt: ' < ?',
|
||||
lte: ' <= ?',
|
||||
contains: ' LIKE ?',
|
||||
in: (values) => ` IN (${values.map(() => '?').join(', ')})`,
|
||||
notIn: (values) => ` NOT IN (${values.map(() => '?').join(', ')})`,
|
||||
};
|
||||
|
||||
// Logical Operators
|
||||
const and = (clauses) => clauses.join(' AND ');
|
||||
const or = (clauses) => clauses.join(' OR ');
|
||||
const parenthetical = (expression) => (expression ? `(${expression})` : '');
|
||||
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { PWBWorker } from 'promise-worker-bi';
|
||||
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
||||
import sharedServiceInit from './utils/shared-service';
|
||||
import {
|
||||
CLEAR_DB,
|
||||
CREATE_TABLES,
|
||||
DELETE_STATEMENT,
|
||||
INSERT_STATEMENTS,
|
||||
} from './utils/schema';
|
||||
|
||||
// Production builds error out when using top level awaits so we'll just use an async IIFE
|
||||
(async () => {
|
||||
/** @type {import('@sqlite.org/sqlite-wasm').OpfsSAHPoolDatabase} */
|
||||
let db;
|
||||
/** @type {import('@sqlite.org/sqlite-wasm').SAHPoolUtil} */
|
||||
let poolUtil;
|
||||
/** @type {import('@sqlite.org/sqlite-wasm').Sqlite3Static} */
|
||||
let sqlite3;
|
||||
|
||||
const { promise: ready, resolve: readyResolve } = Promise.withResolvers();
|
||||
|
||||
// Maximum number of host parameters for SQLite prepared statements.
|
||||
// See "Maximum Number Of Host Parameters In A Single SQL Statement" in
|
||||
// https://www.sqlite.org/limits.html
|
||||
const MAX_HOST_PARAMETERS = 32766;
|
||||
const SCHEMA_VERSION = 1;
|
||||
|
||||
// Some browsers do not allow calling getDirectory in private browsing modes even
|
||||
// if we're in a secure context. This will cause the SQLite setup to fail so we should
|
||||
// check ahead of time to see if we're able to call the API.
|
||||
let canCallGetDirectory;
|
||||
try {
|
||||
canCallGetDirectory = Boolean(await navigator.storage.getDirectory());
|
||||
} catch {
|
||||
canCallGetDirectory = false;
|
||||
}
|
||||
|
||||
const isSecure = self.isSecureContext && canCallGetDirectory;
|
||||
|
||||
// Error code for sqlite corruption errors.
|
||||
// See https://sqlite.org/rescode.html#corrupt
|
||||
const SQLITE_CORRUPT = 11;
|
||||
|
||||
const methods = {
|
||||
initializeSQLite: async () => {
|
||||
sqlite3 = await sqlite3InitModule({
|
||||
print: console.log,
|
||||
printErr: console.error,
|
||||
});
|
||||
const dbName = await promiseWorker.postMessage({
|
||||
method: 'getDatabaseName',
|
||||
});
|
||||
|
||||
function setupDb() {
|
||||
const [row] = db.exec('PRAGMA user_version;', { rowMode: 'object' });
|
||||
// Check if we're on a newer schema version and clear the database if so
|
||||
if (SCHEMA_VERSION > row.user_version) {
|
||||
db.exec(CLEAR_DB);
|
||||
}
|
||||
|
||||
db.exec(CREATE_TABLES(SCHEMA_VERSION));
|
||||
// Force an analyze on initial connection
|
||||
db.exec('PRAGMA optimize=0x10002;');
|
||||
}
|
||||
|
||||
// Instantiate an in memory DB if we're not in a secure context as OPFS is unavailable otherwise
|
||||
if (!isSecure) {
|
||||
db = new sqlite3.oo1.DB(':memory:');
|
||||
setupDb();
|
||||
return;
|
||||
}
|
||||
|
||||
let done = false;
|
||||
while (!done) {
|
||||
try {
|
||||
poolUtil = await sqlite3.installOpfsSAHPoolVfs();
|
||||
db = new poolUtil.OpfsSAHPoolDb(dbName);
|
||||
|
||||
setupDb();
|
||||
done = true;
|
||||
} catch (/** @type {import('@sqlite.org/sqlite-wasm').SQLite3Error} */ err) {
|
||||
// If we get a SQLite3Error and the pool is at capacity, we'll assume the error is
|
||||
// because we hit the cap so we add more capacity and see if that solves it.
|
||||
if (
|
||||
err.name === 'SQLite3Error' &&
|
||||
poolUtil.getCapacity() === poolUtil.getFileCount()
|
||||
) {
|
||||
await poolUtil.addCapacity(1);
|
||||
|
||||
// If we have more than 20 files, that might indicate a problem so let's just
|
||||
// remove the VFS and wipe all the client data across users and restart.
|
||||
if (poolUtil.getCapacity() > 20) {
|
||||
await poolUtil.removeVfs();
|
||||
}
|
||||
} else {
|
||||
console.error('SQLite initialization error:', err);
|
||||
|
||||
// We might have gotten ourselves into a bad state which corrupted the DB so clear it out and retry
|
||||
if (err?.resultCode === SQLITE_CORRUPT) {
|
||||
poolUtil.unlink(`/${dbName}`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
analyzeDatabase: () => {
|
||||
db.exec('PRAGMA optimize');
|
||||
},
|
||||
clearDatabase: () => {
|
||||
db.exec(CLEAR_DB);
|
||||
db.exec(CREATE_TABLES(SCHEMA_VERSION));
|
||||
},
|
||||
deleteDatabase: () => {
|
||||
if (!db) {
|
||||
throw new Error('No database was initialized');
|
||||
}
|
||||
|
||||
const name = db.dbFilename();
|
||||
db.close();
|
||||
db = null;
|
||||
poolUtil.unlink(`/${name}`);
|
||||
},
|
||||
downloadDatabase: () => {
|
||||
return sqlite3.capi.sqlite3_js_db_export(db);
|
||||
},
|
||||
fetchResource: ({ sql, parameters = [] }) => {
|
||||
try {
|
||||
return db.exec({
|
||||
sql,
|
||||
bind: parameters,
|
||||
rowMode: 'object',
|
||||
});
|
||||
} catch (err) {
|
||||
// TODO: Add error handling to rest of methods and properly return the error back to caller
|
||||
console.error('Fetch resource error:', err, sql, parameters);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
insertResource: ({ resource, items, modelMapping }) => {
|
||||
if (items.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const numberOfParameters = items.reduce((acc, item) => {
|
||||
return acc + item.length;
|
||||
}, 0);
|
||||
|
||||
// We want to use as many VALUES as part of the insert statement as it's quicker
|
||||
// than doing multiple inserts. If we have too many rows to insert, we'll manually
|
||||
// chunk them up to avoid hitting the maximum number of host parameters
|
||||
// and execute these separate insert statements in a transaction
|
||||
if (numberOfParameters > MAX_HOST_PARAMETERS) {
|
||||
const chunkSize = Math.floor(MAX_HOST_PARAMETERS / items[0].length);
|
||||
|
||||
return db.transaction(() => {
|
||||
const results = [];
|
||||
for (let i = 0; i < items.length; i += chunkSize) {
|
||||
const chunk = items.slice(i, i + chunkSize);
|
||||
const result = db.exec({
|
||||
sql: INSERT_STATEMENTS(resource, chunk, modelMapping),
|
||||
bind: chunk.flat(),
|
||||
rowMode: 'object',
|
||||
});
|
||||
results.push(result);
|
||||
}
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
db.exec({
|
||||
sql: INSERT_STATEMENTS(resource, items, modelMapping),
|
||||
bind: items.flat(),
|
||||
rowMode: 'object',
|
||||
});
|
||||
},
|
||||
deleteResource: ({ resource, ids }) => {
|
||||
// Check if we have too many parameters for the deletion and chunk if so
|
||||
if (ids?.length > MAX_HOST_PARAMETERS) {
|
||||
return db.transaction(() => {
|
||||
const results = [];
|
||||
for (let i = 0; i < ids.length; i += MAX_HOST_PARAMETERS) {
|
||||
const chunk = ids.slice(i, i + MAX_HOST_PARAMETERS);
|
||||
const result = db.exec({
|
||||
sql: DELETE_STATEMENT(resource, chunk),
|
||||
bind: chunk,
|
||||
rowMode: 'object',
|
||||
});
|
||||
results.push(result);
|
||||
}
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
return db.exec({
|
||||
sql: DELETE_STATEMENT(resource, ids),
|
||||
bind: ids,
|
||||
rowMode: 'object',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const promiseWorker = new PWBWorker();
|
||||
promiseWorker.register(async ({ method, payload }) => {
|
||||
if (!sharedService) {
|
||||
await ready;
|
||||
return methods[method](payload);
|
||||
}
|
||||
|
||||
await sharedService.status.ready;
|
||||
return sharedService.proxy[method](payload);
|
||||
});
|
||||
|
||||
const initializeOnProviderChange = async (isProvider) => {
|
||||
if (!isProvider) {
|
||||
return;
|
||||
}
|
||||
// Initialize the SQLite database when we become a provider
|
||||
await sharedService.proxy.initializeSQLite();
|
||||
};
|
||||
|
||||
// Only use the shared service in a secure context as weblocks aren't available otherwise
|
||||
let sharedService;
|
||||
if (isSecure) {
|
||||
sharedService = sharedServiceInit(
|
||||
'sqlite',
|
||||
methods,
|
||||
initializeOnProviderChange,
|
||||
);
|
||||
} else {
|
||||
await methods.initializeSQLite();
|
||||
readyResolve();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { underscore } from '@ember/string';
|
||||
|
||||
export const CLEAR_DB = `
|
||||
PRAGMA writable_schema = 1;
|
||||
DELETE FROM sqlite_master;
|
||||
PRAGMA writable_schema = 0;
|
||||
VACUUM;
|
||||
PRAGMA integrity_check;`;
|
||||
|
||||
const createTargetTables = `
|
||||
CREATE TABLE IF NOT EXISTS target (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
address TEXT,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_time TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_target_scope_id_created_time ON target(scope_id, created_time DESC);
|
||||
|
||||
-- Create a contentless FTS table as we will only use the rowids.
|
||||
-- Note that this only creates the FTS index and cannot reference the target content
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS target_fts USING fts5(
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
description,
|
||||
address,
|
||||
scope_id,
|
||||
created_time,
|
||||
content='',
|
||||
);
|
||||
|
||||
-- Create triggers to keep the FTS table in sync with the target table
|
||||
-- Note: The FTS table does not have an update trigger, as it's updated only on
|
||||
-- INSERT and DELETE as we do an INSERT OR REPLACE INTO on the table
|
||||
-- which replaces the row and doesn't execute an UPDATE trigger.
|
||||
CREATE TRIGGER IF NOT EXISTS target_ai AFTER INSERT ON target BEGIN
|
||||
INSERT INTO target_fts(
|
||||
rowid, id, type, name, description, address, scope_id, created_time
|
||||
) VALUES (
|
||||
new.rowid, new.id, new.type, new.name, new.description, new.address, new.scope_id, new.created_time
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS target_ad AFTER DELETE ON target BEGIN
|
||||
INSERT INTO target_fts(target_fts, rowid, id, type, name, description, address, scope_id, created_time)
|
||||
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.address, old.scope_id, old.created_time);
|
||||
END;`;
|
||||
|
||||
const createAliasTables = `
|
||||
CREATE TABLE IF NOT EXISTS alias (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
destination_id TEXT,
|
||||
value TEXT,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_time TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_alias_created_time ON alias(created_time DESC);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS alias_fts USING fts5(
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
description,
|
||||
destination_id,
|
||||
value,
|
||||
scope_id,
|
||||
created_time,
|
||||
content='',
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS alias_ai AFTER INSERT ON alias BEGIN
|
||||
INSERT INTO alias_fts(
|
||||
rowid, id, type, name, description, destination_id, value, scope_id, created_time
|
||||
) VALUES (
|
||||
new.rowid, new.id, new.type, new.name, new.description, new.destination_id, new.value, new.scope_id, new.created_time
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS alias_ad AFTER DELETE ON alias BEGIN
|
||||
INSERT INTO alias_fts(alias_fts, rowid, id, type, name, description, destination_id, value, scope_id, created_time)
|
||||
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.destination_id, old.value, old.scope_id, old.created_time);
|
||||
END;`;
|
||||
|
||||
const createGroupTables = `
|
||||
CREATE TABLE IF NOT EXISTS "group" (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_time TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_scope_id_created_time ON "group"(scope_id, created_time DESC);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS group_fts USING fts5(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
scope_id,
|
||||
created_time,
|
||||
content='',
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS group_ai AFTER INSERT ON "group" BEGIN
|
||||
INSERT INTO group_fts(
|
||||
rowid, id, name, description, scope_id, created_time
|
||||
) VALUES (
|
||||
new.rowid, new.id, new.name, new.description, new.scope_id, new.created_time
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS group_ad AFTER DELETE ON "group" BEGIN
|
||||
INSERT INTO group_fts(group_fts, rowid, id, name, description, scope_id, created_time)
|
||||
VALUES('delete', old.rowid, old.id, old.name, old.description, old.scope_id, old.created_time);
|
||||
END;`;
|
||||
|
||||
const createRoleTables = `
|
||||
CREATE TABLE IF NOT EXISTS role (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_time TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_scope_id_created_time ON role(scope_id, created_time DESC);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS role_fts USING fts5(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
scope_id,
|
||||
created_time,
|
||||
content='',
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS role_ai AFTER INSERT ON role BEGIN
|
||||
INSERT INTO role_fts(
|
||||
rowid, id, name, description, scope_id, created_time
|
||||
) VALUES (
|
||||
new.rowid, new.id, new.name, new.description, new.scope_id, new.created_time
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS role_ad AFTER DELETE ON role BEGIN
|
||||
INSERT INTO role_fts(role_fts, rowid, id, name, description, scope_id, created_time)
|
||||
VALUES('delete', old.rowid, old.id, old.name, old.description, old.scope_id, old.created_time);
|
||||
END;`;
|
||||
|
||||
const createUserTables = `
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_time TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_scope_id_created_time ON user(scope_id, created_time DESC);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS user_fts USING fts5(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
scope_id,
|
||||
created_time,
|
||||
content='',
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS user_ai AFTER INSERT ON user BEGIN
|
||||
INSERT INTO user_fts(
|
||||
rowid, id, name, description, scope_id, created_time
|
||||
) VALUES (
|
||||
new.rowid, new.id, new.name, new.description, new.scope_id, new.created_time
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS user_ad AFTER DELETE ON user BEGIN
|
||||
INSERT INTO user_fts(user_fts, rowid, id, name, description, scope_id, created_time)
|
||||
VALUES('delete', old.rowid, old.id, old.name, old.description, old.scope_id, old.created_time);
|
||||
END;`;
|
||||
|
||||
const createCredentialStoreTables = `
|
||||
CREATE TABLE IF NOT EXISTS credential_store (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_time TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_credential_store_scope_id_created_time ON credential_store(scope_id, created_time DESC);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS credential_store_fts USING fts5(
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
description,
|
||||
scope_id,
|
||||
created_time,
|
||||
content='',
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS credential_store_ai AFTER INSERT ON credential_store BEGIN
|
||||
INSERT INTO credential_store_fts(
|
||||
rowid, id, type, name, description, scope_id, created_time
|
||||
) VALUES (
|
||||
new.rowid, new.id, new.type, new.name, new.description, new.scope_id, new.created_time
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS credential_store_ad AFTER DELETE ON credential_store BEGIN
|
||||
INSERT INTO credential_store_fts(credential_store_fts, rowid, id, type, name, description, scope_id, created_time)
|
||||
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.scope_id, old.created_time);
|
||||
END;`;
|
||||
|
||||
const createScopeTables = `
|
||||
CREATE TABLE IF NOT EXISTS scope (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_time TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_scope_scope_id_created_time ON scope(scope_id, created_time DESC);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS scope_fts USING fts5(
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
description,
|
||||
scope_id,
|
||||
created_time,
|
||||
content='',
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS scope_ai AFTER INSERT ON scope BEGIN
|
||||
INSERT INTO scope_fts(
|
||||
rowid, id, type, name, description, scope_id, created_time
|
||||
) VALUES (
|
||||
new.rowid, new.id, new.type, new.name, new.description, new.scope_id, new.created_time
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS scope_ad AFTER DELETE ON scope BEGIN
|
||||
INSERT INTO scope_fts(scope_fts, rowid, id, type, name, description, scope_id, created_time)
|
||||
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.scope_id, old.created_time);
|
||||
END;`;
|
||||
|
||||
const createAuthMethodTables = `
|
||||
CREATE TABLE IF NOT EXISTS auth_method (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
is_primary INT,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_time TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_auth_method_scope_id_created_time ON auth_method(scope_id, created_time DESC);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS auth_method_fts USING fts5(
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
description,
|
||||
is_primary,
|
||||
scope_id,
|
||||
created_time,
|
||||
content='',
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS auth_method_ai AFTER INSERT ON auth_method BEGIN
|
||||
INSERT INTO auth_method_fts(
|
||||
rowid, id, type, name, description, is_primary, scope_id, created_time
|
||||
) VALUES (
|
||||
new.rowid, new.id, new.type, new.name, new.description, new.is_primary, new.scope_id, new.created_time
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS auth_method_ad AFTER DELETE ON auth_method BEGIN
|
||||
INSERT INTO auth_method_fts(auth_method_fts, rowid, id, type, name, description, is_primary, scope_id, created_time)
|
||||
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.is_primary, old.scope_id, old.created_time);
|
||||
END;`;
|
||||
|
||||
const createHostCatalogTables = `
|
||||
CREATE TABLE IF NOT EXISTS host_catalog (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
plugin_name TEXT,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_time TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_host_catalog_scope_id_created_time ON host_catalog(scope_id, created_time DESC);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS host_catalog_fts USING fts5(
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
description,
|
||||
plugin_name,
|
||||
scope_id,
|
||||
created_time,
|
||||
content='',
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS host_catalog_ai AFTER INSERT ON host_catalog BEGIN
|
||||
INSERT INTO host_catalog_fts(
|
||||
rowid, id, type, name, description, plugin_name, scope_id, created_time
|
||||
) VALUES (
|
||||
new.rowid, new.id, new.type, new.name, new.description, new.plugin_name, new.scope_id, new.created_time
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS host_catalog_ad AFTER DELETE ON host_catalog BEGIN
|
||||
INSERT INTO host_catalog_fts(host_catalog_fts, rowid, id, type, name, description, plugin_name, scope_id, created_time)
|
||||
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.plugin_name, old.scope_id, old.created_time);
|
||||
END;`;
|
||||
|
||||
const createSessionRecordingTables = `
|
||||
CREATE TABLE IF NOT EXISTS session_recording (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
state TEXT,
|
||||
start_time TEXT,
|
||||
end_time TEXT,
|
||||
duration TEXT,
|
||||
scope_id TEXT NOT NULL,
|
||||
user_id TEXT,
|
||||
user_name TEXT,
|
||||
target_id TEXT,
|
||||
target_name TEXT,
|
||||
target_scope_id TEXT,
|
||||
target_scope_name TEXT,
|
||||
created_time TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_recording_created_time ON session_recording(created_time DESC);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS session_recording_fts USING fts5(
|
||||
id,
|
||||
type,
|
||||
state,
|
||||
start_time,
|
||||
end_time,
|
||||
duration,
|
||||
scope_id,
|
||||
user_id,
|
||||
user_name,
|
||||
target_id,
|
||||
target_name,
|
||||
target_scope_id,
|
||||
target_scope_name,
|
||||
created_time,
|
||||
content='',
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS session_recording_ai AFTER INSERT ON session_recording BEGIN
|
||||
INSERT INTO session_recording_fts(
|
||||
rowid, id, type, state, start_time, end_time, duration, scope_id, user_id, user_name, target_id, target_name, target_scope_id, target_scope_name, created_time
|
||||
) VALUES (
|
||||
new.rowid, new.id, new.type, new.state, new.start_time, new.end_time, new.duration, new.scope_id, new.user_id, new.user_name, new.target_id, new.target_name, new.target_scope_id, new.target_scope_name, new.created_time
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS session_recording_ad AFTER DELETE ON session_recording BEGIN
|
||||
INSERT INTO session_recording_fts(session_recording_fts, rowid, id, type, state, start_time, end_time, duration, scope_id, user_id, user_name, target_id, target_name, target_scope_id, target_scope_name, created_time)
|
||||
VALUES('delete', old.rowid, old.id, old.type, old.state, old.start_time, old.end_time, old.duration, old.scope_id, old.user_id, old.user_name, old.target_id, old.target_name, old.target_scope_id, old.target_scope_name, old.created_time);
|
||||
END;`;
|
||||
|
||||
const createSessionTables = `
|
||||
CREATE TABLE IF NOT EXISTS session (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
type TEXT,
|
||||
status TEXT,
|
||||
endpoint TEXT,
|
||||
target_id TEXT,
|
||||
user_id TEXT,
|
||||
scope_id TEXT NOT NULL,
|
||||
created_time TEXT NOT NULL,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_scope_id_created_time ON session(scope_id, created_time DESC);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS session_fts USING fts5(
|
||||
id,
|
||||
type,
|
||||
status,
|
||||
endpoint,
|
||||
target_id,
|
||||
user_id,
|
||||
scope_id,
|
||||
created_time,
|
||||
content='',
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS session_ai AFTER INSERT ON session BEGIN
|
||||
INSERT INTO session_fts(
|
||||
rowid, id, type, status, endpoint, target_id, user_id, scope_id, created_time
|
||||
) VALUES (
|
||||
new.rowid, new.id, new.type, new.status, new.endpoint, new.target_id, new.user_id, new.scope_id, new.created_time
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS session_ad AFTER DELETE ON session BEGIN
|
||||
INSERT INTO session_fts(session_fts, rowid, id, type, status, endpoint, target_id, user_id, scope_id, created_time)
|
||||
VALUES('delete', old.rowid, old.id, old.type, old.status, old.endpoint, old.target_id, old.user_id, old.scope_id, old.created_time);
|
||||
END;`;
|
||||
|
||||
export const CREATE_TABLES = (version) => `
|
||||
BEGIN;
|
||||
|
||||
-- This ensures multiple triggers can activate in the scenario of using REPLACE INTO
|
||||
PRAGMA recursive_triggers = true;
|
||||
PRAGMA user_version = ${version};
|
||||
|
||||
CREATE TABLE IF NOT EXISTS token (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
token TEXT NOT NULL
|
||||
);
|
||||
|
||||
${createTargetTables}
|
||||
${createAliasTables}
|
||||
${createGroupTables}
|
||||
${createRoleTables}
|
||||
${createUserTables}
|
||||
${createCredentialStoreTables}
|
||||
${createScopeTables}
|
||||
${createAuthMethodTables}
|
||||
${createHostCatalogTables}
|
||||
${createSessionRecordingTables}
|
||||
${createSessionTables}
|
||||
|
||||
COMMIT;`;
|
||||
|
||||
export const INSERT_STATEMENTS = (resource, items, modelMapping) => {
|
||||
if (resource === 'token') {
|
||||
return `REPLACE INTO token (id, token) VALUES ${items.map(() => '(?, ?)').join(', ')};`;
|
||||
}
|
||||
|
||||
if (!modelMapping?.[resource]) {
|
||||
throw new Error(`modelMapping is required for ${resource} insertions.`);
|
||||
}
|
||||
|
||||
const columns = Object.keys(modelMapping[resource]);
|
||||
// Extra column for the data column
|
||||
const numColumns = Array(columns.length + 1);
|
||||
const placeholders = `(${numColumns.fill('?').join(', ')})`;
|
||||
|
||||
return `REPLACE INTO "${underscore(resource)}" (${columns.join(', ')}, data) VALUES ${items.map(() => placeholders).join(', ')};`;
|
||||
};
|
||||
|
||||
export const DELETE_STATEMENT = (resource, ids) => {
|
||||
const baseDeleteStatement = `DELETE FROM "${underscore(resource)}"`;
|
||||
return ids?.length > 0
|
||||
? `${baseDeleteStatement} WHERE id IN (${ids.map(() => '?').join(',')})`
|
||||
: baseDeleteStatement;
|
||||
};
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
let clientChannel = null;
|
||||
let commonChannel = null;
|
||||
const requestsInFlight = new Map();
|
||||
|
||||
export default (serviceName, target, onProviderChange) => {
|
||||
commonChannel = new BroadcastChannel(
|
||||
`shared-service-common-channel-${serviceName}`,
|
||||
);
|
||||
|
||||
let { promise: ready, resolve: readyResolve } = Promise.withResolvers();
|
||||
const status = {
|
||||
ready,
|
||||
isServiceProvider: false,
|
||||
};
|
||||
|
||||
const proxy = getProxy(target, status);
|
||||
|
||||
// This lock is used to ensure that only one tab can become the provider with an indefinite lock.
|
||||
navigator.locks.request(serviceName, { mode: 'exclusive' }, async () => {
|
||||
// If a client was promoted to provider, reset the ready status and resolver
|
||||
if (readyResolve === null) {
|
||||
const { promise: newReady, resolve: newResolve } =
|
||||
Promise.withResolvers();
|
||||
status.ready = newReady;
|
||||
readyResolve = newResolve;
|
||||
}
|
||||
|
||||
status.isServiceProvider = true;
|
||||
await onBecomeProvider({
|
||||
serviceName,
|
||||
onProviderChange,
|
||||
target,
|
||||
status,
|
||||
});
|
||||
readyResolve();
|
||||
readyResolve = null;
|
||||
await new Promise(() => {});
|
||||
});
|
||||
|
||||
(async () => {
|
||||
let providerExists = false;
|
||||
|
||||
// Make sure a lock was acquired which means we have a provider
|
||||
while (!providerExists) {
|
||||
const locks = await navigator.locks.query();
|
||||
providerExists = locks.held?.some((lock) => lock.name === serviceName);
|
||||
}
|
||||
|
||||
if (!status.isServiceProvider) {
|
||||
await onNotProvider({
|
||||
serviceName,
|
||||
onProviderChange,
|
||||
status,
|
||||
});
|
||||
readyResolve();
|
||||
readyResolve = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return { proxy, status };
|
||||
};
|
||||
|
||||
const getClientBroadcastChannelName = (clientId, serviceName) =>
|
||||
`${clientId}-${serviceName}`;
|
||||
|
||||
// Generate a random ID for the tab to use as a lock name to hold indefinitely until the tab is closed
|
||||
const getClientId = () => {
|
||||
const clientId = uuidv4();
|
||||
|
||||
// Keep the lock until this context is destroyed (which means the tab was closed).
|
||||
navigator.locks.request(clientId, { mode: 'exclusive' }, async () => {
|
||||
await new Promise(() => {});
|
||||
});
|
||||
|
||||
return clientId;
|
||||
};
|
||||
|
||||
// Sends a message to the provider to call the method on behalf of a client.
|
||||
const requestFromProvider = (uuid, resolve, reject, method, args) => {
|
||||
const onRequestListener = getOnRequestListener(uuid, resolve, reject);
|
||||
|
||||
// Listen for a response to know we finished and remove from the list of requests in flight
|
||||
clientChannel?.addEventListener('message', onRequestListener);
|
||||
clientChannel?.postMessage({
|
||||
id: uuid,
|
||||
type: 'request',
|
||||
method,
|
||||
args,
|
||||
});
|
||||
};
|
||||
|
||||
// Return the listener used for responding to requests. Only listens for response messages.
|
||||
const getOnRequestListener = (uuid, resolve, reject) => {
|
||||
const listener = ({ data }) => {
|
||||
const { id, type } = data;
|
||||
if (id !== uuid || type === 'request') {
|
||||
return;
|
||||
}
|
||||
|
||||
requestsInFlight.delete(uuid);
|
||||
clientChannel?.removeEventListener('message', listener);
|
||||
|
||||
if (data.error) {
|
||||
reject(data.error);
|
||||
}
|
||||
|
||||
resolve(data.result);
|
||||
};
|
||||
|
||||
return listener;
|
||||
};
|
||||
|
||||
// When we become a client instead of a provider we do two things:
|
||||
// 1. Register ourselves with the provider and wait for a response that says we registered.
|
||||
// 2. Setup an event listener to know when a provider has changed.
|
||||
const onNotProvider = async ({ serviceName, onProviderChange, status }) => {
|
||||
const clientId = getClientId();
|
||||
clientChannel = new BroadcastChannel(
|
||||
getClientBroadcastChannelName(clientId, serviceName),
|
||||
);
|
||||
|
||||
const register = async () =>
|
||||
new Promise((resolve) => {
|
||||
const onRegisteredListener = ({ data }) => {
|
||||
if (data.clientId === clientId && data.type === 'registered') {
|
||||
commonChannel.removeEventListener('message', onRegisteredListener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// Add an event listener to check for the registration response
|
||||
commonChannel.addEventListener('message', onRegisteredListener);
|
||||
commonChannel.postMessage({ type: 'register', clientId });
|
||||
});
|
||||
|
||||
commonChannel.addEventListener('message', async ({ data }) => {
|
||||
// Check if this was a provider change. If so and we're not the new provider, re-register.
|
||||
if (data.type === 'providerChange' && !status.isServiceProvider) {
|
||||
await onProviderChange?.(status.isServiceProvider);
|
||||
await register();
|
||||
|
||||
// If there were requests in flight when the provider was changed,
|
||||
// we need to resend them to the new provider.
|
||||
if (requestsInFlight.size > 0) {
|
||||
requestsInFlight.forEach(({ method, args, resolve, reject }, uuid) => {
|
||||
requestFromProvider(uuid, resolve, reject, method, args);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await register();
|
||||
};
|
||||
|
||||
// When we become the provider, we do two things:
|
||||
// 1. Set an event listener to check for clients trying to register with us. As part of registration,
|
||||
// we also setup a listener with the individual client to listen for requests.
|
||||
// 2. Send a message to all tabs that we've now become the provider
|
||||
const onBecomeProvider = async ({
|
||||
serviceName,
|
||||
onProviderChange,
|
||||
target,
|
||||
status,
|
||||
}) => {
|
||||
// Set an event listener for other tabs to register with the provider tab
|
||||
commonChannel.addEventListener('message', async (event) => {
|
||||
const { clientId, type } = event.data;
|
||||
if (type !== 'register') {
|
||||
return;
|
||||
}
|
||||
|
||||
const clientChannel = new BroadcastChannel(
|
||||
getClientBroadcastChannelName(clientId, serviceName),
|
||||
);
|
||||
|
||||
// Acquire another lock that immediately is waiting for the
|
||||
// initial lock on the client tab to be resolved to handle cleanup.
|
||||
navigator.locks.request(clientId, { mode: 'exclusive' }, async () => {
|
||||
// The client tab was closed, clean up.
|
||||
clientChannel.close();
|
||||
});
|
||||
|
||||
// Setup an event listener for communication between the provider and subscriber to respond to requests
|
||||
clientChannel.addEventListener('message', async ({ data }) => {
|
||||
if (data.type === 'response') {
|
||||
return;
|
||||
}
|
||||
const { method, args, id } = data;
|
||||
|
||||
let result, error;
|
||||
try {
|
||||
result = await target[method](...args);
|
||||
} catch (e) {
|
||||
// Error objects won't be properly serialized, we need to reconstruct an error like object
|
||||
error =
|
||||
e instanceof Error
|
||||
? Object.fromEntries(
|
||||
Object.getOwnPropertyNames(e).map((k) => [k, e[k]]),
|
||||
)
|
||||
: e;
|
||||
}
|
||||
|
||||
clientChannel.postMessage({
|
||||
id,
|
||||
type: 'response',
|
||||
result,
|
||||
error,
|
||||
method,
|
||||
});
|
||||
});
|
||||
|
||||
// Send back a message to the client that we finished registering
|
||||
commonChannel.postMessage({ type: 'registered', clientId, serviceName });
|
||||
});
|
||||
|
||||
await onProviderChange?.(status.isServiceProvider);
|
||||
commonChannel.postMessage({ type: 'providerChange', serviceName });
|
||||
|
||||
// If we became the new provider and there were requests in flight, request it directly again.
|
||||
if (requestsInFlight.size > 0) {
|
||||
await Promise.all(
|
||||
Array.from(
|
||||
requestsInFlight,
|
||||
async ([uuid, { method, args, resolve, reject }]) => {
|
||||
try {
|
||||
const result = await target[method](...args);
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
requestsInFlight.delete(uuid);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a proxy that we will have all caller methods go through.
|
||||
// If we're the provider, we just call the method directly. If we're not the provider, we'll
|
||||
// send the request through our established client channel to have the provider respond with the data.
|
||||
const getProxy = (target, status) =>
|
||||
new Proxy(target, {
|
||||
get: (target, method) => {
|
||||
if (method === 'then' || method === 'catch' || method === 'finally') {
|
||||
// Return undefined for these methods to allow promise chaining to work correctly
|
||||
return;
|
||||
}
|
||||
|
||||
return async (...args) => {
|
||||
if (status.isServiceProvider) {
|
||||
return await target[method](...args);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const uuid = uuidv4();
|
||||
requestFromProvider(uuid, resolve, reject, method, args);
|
||||
requestsInFlight.set(uuid, { method, args, resolve, reject });
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
@forward 'button';
|
||||
export { default } from 'api/services/sqlite';
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// Ember-data no longer automatically installs BooleanTransform for apps.
|
||||
export { BooleanTransform as default } from '@ember-data/serializer/transform';
|
||||
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default, initialize } from 'core/initializers/deprecations';
|
||||
export { DateTransform as default } from '@ember-data/serializer/transform';
|
||||
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/button';
|
||||
export { NumberTransform as default } from '@ember-data/serializer/transform';
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// Ember-data no longer automatically installs StringTransform for apps.
|
||||
export { StringTransform as default } from '@ember-data/serializer/transform';
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
const Plugin = require('broccoli-plugin');
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const esbuild = require('esbuild');
|
||||
|
||||
module.exports = class BuildWorkers extends Plugin {
|
||||
build() {
|
||||
this.inputPaths.forEach((inputPath) => {
|
||||
this.#buildWorkers(inputPath);
|
||||
});
|
||||
}
|
||||
|
||||
#buildWorkers(inputPath) {
|
||||
const workers = this.#getWorkers(inputPath);
|
||||
|
||||
const workerBuilder = this.#configureWorkerBuilder({
|
||||
isProduction: this.isProduction,
|
||||
buildDir: this.outputPath,
|
||||
});
|
||||
|
||||
Object.entries(workers).map(workerBuilder);
|
||||
}
|
||||
|
||||
#getWorkers(inputPath) {
|
||||
let workers = {};
|
||||
let dir = fs.readdirSync(inputPath);
|
||||
|
||||
dir
|
||||
.filter((name) => name.endsWith('-worker.js'))
|
||||
.forEach((name) => {
|
||||
workers[name] = path.join(inputPath, name);
|
||||
});
|
||||
|
||||
return workers;
|
||||
}
|
||||
|
||||
#configureWorkerBuilder({ isProduction, buildDir }) {
|
||||
return ([name, entryPath]) => {
|
||||
esbuild.buildSync({
|
||||
loader: { '.js': 'js' },
|
||||
entryPoints: [entryPath],
|
||||
bundle: true,
|
||||
outfile: path.join(buildDir, name),
|
||||
format: 'esm',
|
||||
minify: isProduction,
|
||||
sourcemap: !isProduction,
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -6,7 +6,6 @@
|
||||
'use strict';
|
||||
|
||||
const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
|
||||
const { maybeEmbroider } = require('@embroider/test-setup');
|
||||
|
||||
module.exports = function (defaults) {
|
||||
const app = new EmberAddon(defaults, {
|
||||
@@ -19,6 +18,8 @@ module.exports = function (defaults) {
|
||||
This build file does *not* influence how the addon or the app using it
|
||||
behave. You most likely want to be modifying `./index.js` or app's build file
|
||||
*/
|
||||
|
||||
const { maybeEmbroider } = require('@embroider/test-setup');
|
||||
return maybeEmbroider(app, {
|
||||
skipBabel: [
|
||||
{
|
||||
|
||||
@@ -5,6 +5,69 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const { WatchedDir } = require('broccoli-source');
|
||||
const funnel = require('broccoli-funnel');
|
||||
const merge = require('broccoli-merge-trees');
|
||||
const path = require('path');
|
||||
|
||||
const buildWorkers = require('./broccoli-plugins/build-workers');
|
||||
|
||||
const workerRoot = path.join(__dirname, 'addon', 'workers');
|
||||
|
||||
module.exports = {
|
||||
name: require('./package').name,
|
||||
|
||||
options: {
|
||||
// Don't automatically import sqlite, we need to manually control how it gets used
|
||||
autoImport: {
|
||||
exclude: ['@sqlite.org/sqlite-wasm'],
|
||||
},
|
||||
},
|
||||
|
||||
treeForAddon() {
|
||||
// Exclude anything in the workers folder from being bundled in the final
|
||||
// build as we're manually bundling the files ourselves below.
|
||||
const tree = this._super.treeForAddon.apply(this, arguments);
|
||||
return funnel(tree, {
|
||||
exclude: ['api/workers/**/*'],
|
||||
});
|
||||
},
|
||||
|
||||
treeForPublic(tree) {
|
||||
const env = this.parent?.app?.env ?? 'production';
|
||||
const { enableSqlite } = this.parent?.app?.options[this.name] ?? {};
|
||||
|
||||
// If sqlite is not enabled and we're not running a test directly in the addon,
|
||||
// we don't need to build the workers
|
||||
if (!enableSqlite && this.app?.env !== 'test') {
|
||||
return tree;
|
||||
}
|
||||
|
||||
// We need to process and build our workers separately from ember as these
|
||||
// workers are not part of the app and are not getting properly bundled otherwise
|
||||
let workerTree = new WatchedDir(workerRoot);
|
||||
workerTree = new buildWorkers([workerTree]);
|
||||
workerTree.isProduction = env === 'production';
|
||||
|
||||
const workers = funnel(workerTree, {
|
||||
destDir: 'workers',
|
||||
});
|
||||
|
||||
// Also include the sqlite wasm file directly to be placed next to the workers
|
||||
const sqliteWasmDirectoryPath = path.resolve(
|
||||
require.resolve('@sqlite.org/sqlite-wasm/sqlite3.wasm'),
|
||||
'..',
|
||||
);
|
||||
const wasmFile = funnel(sqliteWasmDirectoryPath, {
|
||||
files: ['sqlite3.wasm'],
|
||||
destDir: 'workers',
|
||||
});
|
||||
|
||||
const trees = [workers, wasmFile];
|
||||
if (tree) {
|
||||
trees.push(tree);
|
||||
}
|
||||
|
||||
return merge(trees);
|
||||
},
|
||||
};
|
||||
@@ -70,11 +70,16 @@ function routes() {
|
||||
let resultSet;
|
||||
// Default parent scope is global
|
||||
if (!scope_id) scope_id = 'global';
|
||||
// We do not return global scope to mimic our API.
|
||||
if (recursive && scope_id === 'global') {
|
||||
resultSet = scopes.all();
|
||||
resultSet = scopes.where((scope) => {
|
||||
return scope.scope ? scope.type !== 'global' : false;
|
||||
});
|
||||
} else {
|
||||
resultSet = scopes.where((scope) => {
|
||||
return scope.scope ? scope.scope.id === scope_id : false;
|
||||
return scope.scope
|
||||
? scope.scope.id === scope_id && scope.type !== 'global'
|
||||
: false;
|
||||
});
|
||||
}
|
||||
const filteredResultSet = resultSet.filter(makeBooleanFilter(filter));
|
||||
|
||||
@@ -50,7 +50,8 @@ export default factory.extend({
|
||||
}
|
||||
case TYPE_AUTH_METHOD_OIDC:
|
||||
return {
|
||||
issuer: faker.internet.ip(),
|
||||
issuer:
|
||||
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
|
||||
subject: 'sub',
|
||||
email: faker.internet.email(),
|
||||
full_name: faker.person.fullName(),
|
||||
|
||||
@@ -10,6 +10,7 @@ import generateId from '../helpers/id';
|
||||
import {
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
|
||||
TYPES_CREDENTIAL_LIBRARY,
|
||||
options,
|
||||
} from 'api/models/credential-library';
|
||||
@@ -22,6 +23,9 @@ export default factory.extend({
|
||||
if (this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC) {
|
||||
return options.credential_types[i % options.credential_types.length];
|
||||
}
|
||||
if (this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP) {
|
||||
return options.credential_types[2]; // Only supports username, password & domain credential type
|
||||
}
|
||||
},
|
||||
authorized_actions: () =>
|
||||
permissions.authorizedActionsFor('credential-library') || [
|
||||
@@ -45,6 +49,10 @@ export default factory.extend({
|
||||
key_bits: faker.number.int(999),
|
||||
path: faker.system.directoryPath(),
|
||||
};
|
||||
case TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP:
|
||||
return {
|
||||
path: faker.system.directoryPath(),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -39,7 +39,8 @@ export default factory.extend({
|
||||
switch (this.type) {
|
||||
case 'vault':
|
||||
return {
|
||||
address: faker.internet.ip(),
|
||||
address:
|
||||
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
|
||||
namespace: faker.word.words(1),
|
||||
ca_cert: 'ca-cert-123',
|
||||
tls_server_name: faker.internet.domainName(),
|
||||
@@ -57,7 +58,7 @@ export default factory.extend({
|
||||
const { scope, type } = credentialStore;
|
||||
switch (type) {
|
||||
case 'vault':
|
||||
server.createList('credential-library', 2, {
|
||||
server.createList('credential-library', 3, {
|
||||
scope,
|
||||
credentialStore,
|
||||
});
|
||||
|
||||
@@ -7,8 +7,14 @@ import factory from '../generated/factories/credential';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import permissions from '../helpers/permissions';
|
||||
import generatedId from '../helpers/id';
|
||||
import {
|
||||
TYPES_CREDENTIAL,
|
||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||
} from 'api/models/credential';
|
||||
|
||||
const types = ['username_password', 'ssh_private_key', 'json'];
|
||||
const types = [...TYPES_CREDENTIAL];
|
||||
|
||||
export default factory.extend({
|
||||
type: (i) => types[i % types.length],
|
||||
@@ -20,6 +26,8 @@ export default factory.extend({
|
||||
return generatedId('credup_');
|
||||
case 'json':
|
||||
return generatedId('credjson_');
|
||||
case 'username_password_domain':
|
||||
return generatedId('credupd_');
|
||||
}
|
||||
},
|
||||
authorized_actions: () =>
|
||||
@@ -30,8 +38,21 @@ export default factory.extend({
|
||||
'delete',
|
||||
],
|
||||
attributes() {
|
||||
return {
|
||||
username: faker.internet.userName(),
|
||||
};
|
||||
switch (this.type) {
|
||||
case TYPE_CREDENTIAL_USERNAME_PASSWORD:
|
||||
return { username: faker.internet.userName() };
|
||||
|
||||
case TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN:
|
||||
return {
|
||||
username: faker.internet.userName(),
|
||||
domain: faker.internet.domainName(),
|
||||
};
|
||||
|
||||
case TYPE_CREDENTIAL_SSH_PRIVATE_KEY:
|
||||
return { username: faker.internet.userName() };
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -8,19 +8,29 @@ import { trait } from 'miragejs';
|
||||
import permissions from '../helpers/permissions';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import generateId from '../helpers/id';
|
||||
import {
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_STATIC,
|
||||
TYPE_HOST_CATALOG_DYNAMIC,
|
||||
TYPES_HOST_CATALOG,
|
||||
} from 'api/models/host-catalog';
|
||||
|
||||
const types = ['static', 'plugin'];
|
||||
// Represents known plugin types, except "foobar" which models the possibility
|
||||
// of receiving an _unknown_ type, which the UI must handle gracefully.
|
||||
const pluginTypes = ['aws', 'azure', 'foobar'];
|
||||
|
||||
let pluginTypeCounter = 1;
|
||||
const pluginTypes = [
|
||||
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
'foobar',
|
||||
];
|
||||
|
||||
export default factory.extend({
|
||||
id: () => generateId('hc_'),
|
||||
|
||||
// Cycle through available types
|
||||
type: (i) => types[i % types.length],
|
||||
type: (i) => TYPES_HOST_CATALOG[i % TYPES_HOST_CATALOG.length],
|
||||
|
||||
authorized_actions: () =>
|
||||
permissions.authorizedActionsFor('host-catalog') || [
|
||||
@@ -30,7 +40,7 @@ export default factory.extend({
|
||||
'delete',
|
||||
],
|
||||
authorized_collection_actions: function () {
|
||||
const isStatic = this.type === 'static';
|
||||
const isStatic = this.type === TYPE_HOST_CATALOG_STATIC;
|
||||
return {
|
||||
// only static catalogs allow host create at this time
|
||||
hosts: isStatic ? ['create', 'list'] : ['list'],
|
||||
@@ -39,16 +49,20 @@ export default factory.extend({
|
||||
},
|
||||
|
||||
worker_filter: function () {
|
||||
if (this.type === 'plugin' && this.plugin?.name === 'aws') {
|
||||
if (
|
||||
this.type === TYPE_HOST_CATALOG_DYNAMIC &&
|
||||
(this.plugin?.name === TYPE_HOST_CATALOG_PLUGIN_AWS ||
|
||||
this.plugin?.name === TYPE_HOST_CATALOG_PLUGIN_GCP)
|
||||
) {
|
||||
return `"${faker.word.noun()}" in "${faker.system.directoryPath()}"`;
|
||||
}
|
||||
},
|
||||
|
||||
plugin: function (i) {
|
||||
if (this.type === 'plugin') {
|
||||
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
|
||||
return {
|
||||
id: `plugin-id-${i}`,
|
||||
name: pluginTypes[pluginTypeCounter++ % pluginTypes.length],
|
||||
name: pluginTypes[i % pluginTypes.length],
|
||||
description: faker.word.words(),
|
||||
};
|
||||
}
|
||||
@@ -56,35 +70,50 @@ export default factory.extend({
|
||||
|
||||
attributes() {
|
||||
switch (this.plugin?.name) {
|
||||
case 'aws':
|
||||
case TYPE_HOST_CATALOG_PLUGIN_AWS:
|
||||
return {
|
||||
disable_credential_rotation: faker.datatype.boolean(),
|
||||
region: `us-${faker.location.cardinalDirection()}-${faker.number.int(
|
||||
9,
|
||||
)}`,
|
||||
};
|
||||
case 'azure':
|
||||
case TYPE_HOST_CATALOG_PLUGIN_AZURE:
|
||||
return {
|
||||
disable_credential_rotation: faker.datatype.boolean(),
|
||||
tenant_id: faker.string.uuid(),
|
||||
client_id: faker.string.uuid(),
|
||||
subscription_id: faker.string.uuid(),
|
||||
};
|
||||
case TYPE_HOST_CATALOG_PLUGIN_GCP:
|
||||
return {
|
||||
disable_credential_rotation: faker.datatype.boolean(),
|
||||
project_id: faker.string.uuid(),
|
||||
client_email: faker.internet.email(),
|
||||
target_service_account_id: faker.string.uuid(),
|
||||
zone: `us-${faker.location.cardinalDirection()}${faker.number.int(
|
||||
9,
|
||||
)}-${faker.string.fromCharacters('abc')}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
secrets() {
|
||||
switch (this.plugin?.name) {
|
||||
case 'aws':
|
||||
case TYPE_HOST_CATALOG_PLUGIN_AWS:
|
||||
return {
|
||||
access_key_id: faker.string.nanoid(),
|
||||
secret_access_key: faker.string.nanoid(),
|
||||
};
|
||||
case 'azure':
|
||||
case TYPE_HOST_CATALOG_PLUGIN_AZURE:
|
||||
return {
|
||||
secret_id: faker.string.nanoid(),
|
||||
secret_value: faker.string.nanoid(),
|
||||
};
|
||||
case TYPE_HOST_CATALOG_PLUGIN_GCP:
|
||||
return {
|
||||
private_key: faker.string.nanoid(),
|
||||
private_key_id: faker.string.nanoid(),
|
||||
};
|
||||
}
|
||||
},
|
||||
withChildren: trait({
|
||||
|
||||
Loaded 100 of 1685 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user