Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd3fc5cea7 | ||
|
|
719bab7358 | ||
|
|
95f72e1248 | ||
|
|
f9a9f677ca | ||
|
|
dd0c5df25c | ||
|
|
4fc3244049 | ||
|
|
5faf830d46 | ||
|
|
d2cf82b85c | ||
|
|
fb0020c395 | ||
|
|
e55ea0b629 | ||
|
|
9234dbae70 | ||
|
|
27136eaa71 | ||
|
|
6554af1164 | ||
|
|
437d0e686d | ||
|
|
25b801826d | ||
|
|
123818dc70 | ||
|
|
dafcd046d7 | ||
|
|
accf9faf60 | ||
|
|
6bb237e78c | ||
|
|
bbb5808875 | ||
|
|
905a8f1951 | ||
|
|
f39961bbf5 | ||
|
|
2bf09011fb | ||
|
|
013aa2e570 | ||
|
|
953e35bcd6 | ||
|
|
e138b06fd3 | ||
|
|
2ef6b0549d | ||
|
|
1c49303432 | ||
|
|
f31b1e975d | ||
|
|
48d0087141 | ||
|
|
58cc9e56fc | ||
|
|
189a453e8b | ||
|
|
6fca90747c | ||
|
|
92f5a65c00 | ||
|
|
d8744e81bc | ||
|
|
136baf8260 | ||
|
|
ef47c163d0 | ||
|
|
39e5e3406b | ||
|
|
91d8d61afb | ||
|
|
b1d9153efe | ||
|
|
fda1f56ad1 | ||
|
|
fa18bfff01 | ||
|
|
72f7c8a92d | ||
|
|
838bc37243 | ||
|
|
866972c6c6 | ||
|
|
b19d76f249 | ||
|
|
35ccaeba56 | ||
|
|
bf134072e5 | ||
|
|
0bd152e0b5 | ||
|
|
928ca5409c | ||
|
|
0f846eab2c | ||
|
|
14ed74dd3d | ||
|
|
701d19bd18 | ||
|
|
240a544d82 | ||
|
|
b4f3741979 | ||
|
|
396bf0a43f | ||
|
|
3225ccda10 | ||
|
|
ad2c1ba2ad | ||
|
|
ea591df0bb |
No files matched your search
@@ -13,14 +13,14 @@ jobs:
|
|||||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
steps:
|
steps:
|
||||||
- uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1.7.3
|
- uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0
|
||||||
with:
|
with:
|
||||||
install-dependencies: true
|
install-dependencies: true
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
- name: Output ember a11y test report summary (desktop)
|
- 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? })}) })"'
|
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
|
- name: Upload ember a11y reports
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: ember-a11y-reports
|
name: ember-a11y-reports
|
||||||
path: ./ember-a11y-reports
|
path: ./ember-a11y-reports
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ jobs:
|
|||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -26,12 +26,12 @@ jobs:
|
|||||||
needs: [dependencies]
|
needs: [dependencies]
|
||||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -39,12 +39,12 @@ jobs:
|
|||||||
- run: pnpm install --offline
|
- run: pnpm install --offline
|
||||||
- run: pnpm run build:ui:admin:oss
|
- run: pnpm run build:ui:admin:oss
|
||||||
- name: Upload artifact admin-ui
|
- name: Upload artifact admin-ui
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: admin-ui
|
name: admin-ui
|
||||||
path: ./ui/admin/dist/
|
path: ./ui/admin/dist/
|
||||||
- name: Upload artifact OSS admin UI
|
- name: Upload artifact OSS admin UI
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: admin-ui-oss
|
name: admin-ui-oss
|
||||||
path: ./ui/admin/dist/
|
path: ./ui/admin/dist/
|
||||||
@@ -53,12 +53,12 @@ jobs:
|
|||||||
needs: [dependencies]
|
needs: [dependencies]
|
||||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
- run: pnpm install --offline
|
- run: pnpm install --offline
|
||||||
- run: pnpm run build:ui:admin:enterprise
|
- run: pnpm run build:ui:admin:enterprise
|
||||||
- name: Upload artifact enterprise admin UI
|
- name: Upload artifact enterprise admin UI
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: admin-ui-ent
|
name: admin-ui-ent
|
||||||
path: ./ui/admin/dist/
|
path: ./ui/admin/dist/
|
||||||
@@ -75,12 +75,12 @@ jobs:
|
|||||||
needs: [dependencies]
|
needs: [dependencies]
|
||||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
- run: pnpm install --offline
|
- run: pnpm install --offline
|
||||||
- run: pnpm run build:ui:admin:hcp
|
- run: pnpm run build:ui:admin:hcp
|
||||||
- name: Upload artifact HCP admin UI
|
- name: Upload artifact HCP admin UI
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: admin-ui-hcp
|
name: admin-ui-hcp
|
||||||
path: ./ui/admin/dist/
|
path: ./ui/admin/dist/
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
# Ref and repository aren't normally needed, this is a workaround for a bug in auto commit
|
# Ref and repository aren't normally needed, this is a workaround for a bug in auto commit
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
@@ -14,6 +14,6 @@ jobs:
|
|||||||
- uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3
|
- uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3
|
||||||
- name: Add missing Copyright Headers
|
- name: Add missing Copyright Headers
|
||||||
run: copywrite headers
|
run: copywrite headers
|
||||||
- uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1
|
- uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
|
||||||
with:
|
with:
|
||||||
commit_message: Add missing copyright headers
|
commit_message: Add missing copyright headers
|
||||||
@@ -6,6 +6,6 @@ jobs:
|
|||||||
triage:
|
triage:
|
||||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
@@ -6,11 +6,11 @@ jobs:
|
|||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -22,11 +22,11 @@ jobs:
|
|||||||
needs: [dependencies]
|
needs: [dependencies]
|
||||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -40,11 +40,11 @@ jobs:
|
|||||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -61,11 +61,11 @@ jobs:
|
|||||||
- uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0
|
- uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0
|
||||||
with:
|
with:
|
||||||
install-dependencies: true
|
install-dependencies: true
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ jobs:
|
|||||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
|
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
|
||||||
- run: |
|
- run: |
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
name: Test E2E Admin UI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [labeled, opened, synchronize, reopened]
|
||||||
|
paths:
|
||||||
|
- 'ui/admin/**'
|
||||||
|
- 'e2e-tests/*'
|
||||||
|
- 'e2e-tests/api-client/**'
|
||||||
|
- 'e2e-tests/helpers/**'
|
||||||
|
- 'e2e-tests/admin/**'
|
||||||
|
- '.github/workflows/test-e2e-admin-ui.yaml'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
boundary-enterprise-branch:
|
||||||
|
description: 'Branch of boundary-enterprise to test against (default to main)'
|
||||||
|
required: true
|
||||||
|
default: 'main'
|
||||||
|
type: string
|
||||||
|
boundary-branch:
|
||||||
|
description: 'Branch of boundary to test against (default to main)'
|
||||||
|
required: true
|
||||||
|
default: 'main'
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
runs-on: ${{ fromJSON(vars.RUNNER_UBUNTU_22) }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- boundary_edition: community
|
||||||
|
github_repo: hashicorp/boundary
|
||||||
|
folder_name: boundary
|
||||||
|
boundary_edition_short: oss
|
||||||
|
enos_scenario: e2e_ui_docker
|
||||||
|
test_command: admin:ce:docker
|
||||||
|
boundary_branch: ${{ github.event.inputs.boundary-branch || 'main' }}
|
||||||
|
- boundary_edition: enterprise
|
||||||
|
github_repo: hashicorp/boundary-enterprise
|
||||||
|
folder_name: boundary-enterprise
|
||||||
|
boundary_edition_short: ent
|
||||||
|
enos_scenario: e2e_ui_docker_ent
|
||||||
|
test_command: admin:ent:docker
|
||||||
|
boundary_branch: ${{ github.event.inputs.boundary-enterprise-branch || 'main' }}
|
||||||
|
fail-fast: false
|
||||||
|
env:
|
||||||
|
ENOS_VAR_e2e_debug_no_run: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@39a8f388434bea5eeba6649b99aa9949a55a1150
|
||||||
|
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: GPG setup
|
||||||
|
run: |
|
||||||
|
# Create a GPG key
|
||||||
|
export KEY_PW=boundary
|
||||||
|
gpg --generate-key --batch <<eoGpgConf
|
||||||
|
%echo Started!
|
||||||
|
Key-Type: RSA
|
||||||
|
Key-Length: default
|
||||||
|
Subkey-Type: RSA
|
||||||
|
Name-Real: boundary
|
||||||
|
Name-Comment: default
|
||||||
|
Name-Email: default
|
||||||
|
Expire-Date: 0
|
||||||
|
Passphrase: $KEY_PW
|
||||||
|
%commit
|
||||||
|
%echo Done.
|
||||||
|
eoGpgConf
|
||||||
|
|
||||||
|
# Enable gpg-preset-passphrase so that key passwords can be saved
|
||||||
|
echo "allow-preset-passphrase" >> ~/.gnupg/gpg-agent.conf
|
||||||
|
gpg-connect-agent reloadagent /bye &>/dev/null
|
||||||
|
|
||||||
|
# Get information about the created keys
|
||||||
|
export lines=$(gpg --list-secret-keys --with-colons --with-keygrip)
|
||||||
|
export KEY_ID=""
|
||||||
|
while read -r line
|
||||||
|
do
|
||||||
|
# Save the first key id to be used later
|
||||||
|
if [[ $line =~ "fpr"* ]]; then
|
||||||
|
if [[ $KEY_ID == "" ]]; then
|
||||||
|
KEY_ID="$(echo "$line" | sed -r 's/fpr|://g')"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cache the passphrases for the keys so passwords do not need to be entered
|
||||||
|
if [[ $line =~ "grp"* ]]; then
|
||||||
|
export KEYGRIP_ID="$(echo "$line" | sed -r 's/grp|://g')"
|
||||||
|
/usr/lib/gnupg/gpg-preset-passphrase --preset -P $KEY_PW $KEYGRIP_ID
|
||||||
|
fi
|
||||||
|
done <<< $lines
|
||||||
|
|
||||||
|
# Trust the key
|
||||||
|
touch /tmp/test.txt
|
||||||
|
gpg -a --encrypt -r $KEY_ID --trust-model always --batch --yes /tmp/test.txt
|
||||||
|
echo "trusted-key $KEY_ID" >> ~/.gnupg/gpg.conf
|
||||||
|
|
||||||
|
# Initialize the password store
|
||||||
|
pass init $KEY_ID &>/dev/null
|
||||||
|
|
||||||
|
- name: Checkout ${{ matrix.folder_name }} repo
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
repository: ${{ matrix.github_repo }}
|
||||||
|
path: support/src/${{ matrix.folder_name }}
|
||||||
|
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
|
||||||
|
ref: ${{ matrix.boundary_branch }}
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- name: Set git config
|
||||||
|
run: |
|
||||||
|
git config --global url."https://oauth2:${{ secrets.ELEVATED_GITHUB_TOKEN }}@github.com".insteadOf "https://github.com"
|
||||||
|
|
||||||
|
- name: Install node dependencies
|
||||||
|
run: |
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
cd e2e-tests
|
||||||
|
pnpm playwright install --with-deps
|
||||||
|
|
||||||
|
- name: Set up test infra
|
||||||
|
id: infra
|
||||||
|
run: |
|
||||||
|
ssh-keygen -N '' -t ed25519 -f ~/.ssh/github_enos
|
||||||
|
mkdir -p ./enos/terraform-plugin-cache
|
||||||
|
echo ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} > ./support/src/${{matrix.folder_name}}/internal/ui/VERSION
|
||||||
|
export ENOS_VAR_ui_build_override="oss" && \
|
||||||
|
export ENOS_VAR_boundary_edition=${{ matrix.boundary_edition_short }} && \
|
||||||
|
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
||||||
|
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
|
||||||
|
export ENOS_VAR_boundary_license="${{ secrets.BOUNDARY_ENT_LICENSE }}" && \
|
||||||
|
enos scenario launch --timeout 60m0s --chdir ./support/src/${{matrix.folder_name}}/enos ${{matrix.enos_scenario}} builder:local
|
||||||
|
|
||||||
|
- name: Move boundary binary
|
||||||
|
run: |
|
||||||
|
mv ./support/src/${{matrix.folder_name}}/bin/boundary /usr/local/bin/
|
||||||
|
boundary version
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: |
|
||||||
|
source <(bash ./support/src/${{matrix.folder_name}}/enos/scripts/test_e2e_env.sh)
|
||||||
|
cd e2e-tests
|
||||||
|
pnpm run ${{ matrix.test_command }} --reporter=html
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
|
with:
|
||||||
|
name: playwright-report-${{ matrix.boundary_edition }}
|
||||||
|
path: e2e-tests/playwright-report
|
||||||
|
|
||||||
|
- name: Clean up test infra
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
export ENOS_VAR_boundary_edition=${{ matrix.boundary_edition_short }} && \
|
||||||
|
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/${{matrix.folder_name}}/enos ${{matrix.enos_scenario}} builder:local
|
||||||
|
|
||||||
|
- name: Send Slack message on failure
|
||||||
|
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
|
||||||
|
if: ${{ failure() && github.repository == 'hashicorp/boundary-ui' }}
|
||||||
|
with:
|
||||||
|
method: chat.postMessage
|
||||||
|
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
payload: |
|
||||||
|
channel: ${{ secrets.SLACK_CHANNEL_ID }}
|
||||||
|
text: ":x: admin ui e2e tests failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Branch:* ${{ github.repository }}:${{ github.head_ref }}"
|
||||||
@@ -3,30 +3,35 @@ name: Test E2E Desktop Client
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [labeled, opened, synchronize, reopened]
|
types: [labeled, opened, synchronize, reopened]
|
||||||
|
paths:
|
||||||
|
- 'ui/desktop/**'
|
||||||
|
- 'e2e-tests/*'
|
||||||
|
- 'e2e-tests/api-client/**'
|
||||||
|
- 'e2e-tests/helpers/**'
|
||||||
|
- 'e2e-tests/desktop/**'
|
||||||
|
- '.github/workflows/test-e2e-desktop.yaml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
if: ${{contains(github.event.pull_request.labels.*.name, 'desktop')}}
|
name: test
|
||||||
name: Run Desktop E2E Tests
|
|
||||||
runs-on: ${{ fromJSON(vars.RUNNER_UBUNTU_22) }}
|
runs-on: ${{ fromJSON(vars.RUNNER_UBUNTU_22) }}
|
||||||
env:
|
env:
|
||||||
ENOS_VAR_boundary_edition: "ent"
|
ENOS_VAR_boundary_edition: "ent"
|
||||||
ENOS_VAR_e2e_debug_no_run: true
|
ENOS_VAR_e2e_debug_no_run: true
|
||||||
ENOS_VAR_boundary_license_path: "./license.hclic"
|
|
||||||
SETUP_CLI: true
|
SETUP_CLI: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up pnpm
|
- name: Set up pnpm
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
@@ -38,7 +43,7 @@ jobs:
|
|||||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd
|
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd
|
||||||
|
|
||||||
- name: Set up Enos
|
- name: Set up Enos
|
||||||
uses: hashicorp/action-setup-enos@b9fa53484a1e8fdcc7b02a118bcf01d65b9414c9
|
uses: hashicorp/action-setup-enos@39a8f388434bea5eeba6649b99aa9949a55a1150
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -48,7 +53,7 @@ jobs:
|
|||||||
unzip /tmp/vault.zip -d /usr/local/bin
|
unzip /tmp/vault.zip -d /usr/local/bin
|
||||||
|
|
||||||
- name: Checkout boundary-enterprise
|
- name: Checkout boundary-enterprise
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
repository: hashicorp/boundary-enterprise
|
repository: hashicorp/boundary-enterprise
|
||||||
# ref: ${{ github.event.inputs.boundary-enterprise-branch }}
|
# ref: ${{ github.event.inputs.boundary-enterprise-branch }}
|
||||||
@@ -72,16 +77,14 @@ jobs:
|
|||||||
docker pull hashicorp/boundary-enterprise:latest
|
docker pull hashicorp/boundary-enterprise:latest
|
||||||
docker save hashicorp/boundary-enterprise:latest | gzip > /tmp/boundary-enterprise.zip
|
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
|
- name: Set up test infra
|
||||||
id: infra
|
id: infra
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.BOUNDARY_ENT_LICENSE }}" > ./support/src/boundary-enterprise/enos/license.hclic
|
|
||||||
ssh-keygen -N '' -t ed25519 -f ~/.ssh/github_enos
|
ssh-keygen -N '' -t ed25519 -f ~/.ssh/github_enos
|
||||||
mkdir -p ./enos/terraform-plugin-cache
|
mkdir -p ./enos/terraform-plugin-cache
|
||||||
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
||||||
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
|
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_license="${{ secrets.BOUNDARY_ENT_LICENSE }}" && \
|
||||||
export ENOS_VAR_boundary_docker_image_file=/tmp/boundary-enterprise.zip && \
|
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
|
enos scenario launch --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:crt
|
||||||
|
|
||||||
@@ -102,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Playwright report
|
- name: Upload Playwright report
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: e2e-tests/playwright-report
|
path: e2e-tests/playwright-report
|
||||||
@@ -113,3 +116,13 @@ jobs:
|
|||||||
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
||||||
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
|
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
|
enos scenario destroy --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:crt
|
||||||
|
|
||||||
|
- name: Send Slack message on failure
|
||||||
|
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
|
||||||
|
if: ${{ failure() && github.repository == 'hashicorp/boundary-ui' }}
|
||||||
|
with:
|
||||||
|
method: chat.postMessage
|
||||||
|
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
payload: |
|
||||||
|
channel: ${{ secrets.SLACK_CHANNEL_ID }}
|
||||||
|
text: ":x: desktop client e2e tests failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Branch:* ${{ github.repository }}:${{ github.head_ref }}"
|
||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
trigger-gha-release:
|
trigger-gha-release:
|
||||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Release description
|
- name: Release description
|
||||||
run: |
|
run: |
|
||||||
echo "Triggering release with a TAG ${GITHUB_REF#refs/tags/}"
|
echo "Triggering release with a TAG ${GITHUB_REF#refs/tags/}"
|
||||||
|
|||||||
Binary file not shown.
@@ -26,6 +26,7 @@ module.exports = {
|
|||||||
browser: true,
|
browser: true,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
curly: ['error', 'multi-line', 'consistent'],
|
||||||
'ember/no-get': 'off',
|
'ember/no-get': 'off',
|
||||||
'ember/no-get-with-default': 'off',
|
'ember/no-get-with-default': 'off',
|
||||||
'ember/no-computed-properties-in-native-classes': 'off',
|
'ember/no-computed-properties-in-native-classes': 'off',
|
||||||
|
|||||||
@@ -59,10 +59,11 @@ export default class RoleAbility extends ModelAbility {
|
|||||||
const type = this.model.constructor.modelName;
|
const type = this.model.constructor.modelName;
|
||||||
const typeIsValid =
|
const typeIsValid =
|
||||||
type === 'user' || type === 'group' || type === 'managed-group';
|
type === 'user' || type === 'group' || type === 'managed-group';
|
||||||
if (!typeIsValid)
|
if (!typeIsValid) {
|
||||||
throw new InvalidRolePrincipalTypeError(
|
throw new InvalidRolePrincipalTypeError(
|
||||||
`Expected a role principal of type 'user', 'group', or 'managed-group'. Got '${type}'.`,
|
`Expected a role principal of type 'user', 'group', or 'managed-group'. Got '${type}'.`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return this.can.can(`read ${type}`, this.model);
|
return this.can.can(`read ${type}`, this.model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,14 +76,15 @@ export default class GeneratedCredentialLibraryModel extends BaseModel {
|
|||||||
path;
|
path;
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
for: ['vault-generic', 'vault-ldap'],
|
for: 'vault-generic',
|
||||||
description: 'It indicates the type of credential the library returns.',
|
description: 'It indicates the type of credential the library returns.',
|
||||||
})
|
})
|
||||||
credential_type;
|
credential_type;
|
||||||
|
|
||||||
@attr('object-as-array', {
|
@attr('object', {
|
||||||
for: ['vault-generic', 'vault-ldap'],
|
for: 'vault-generic',
|
||||||
description: 'It indicates the credential mapping overrides.',
|
description: 'It indicates the credential mapping overrides.',
|
||||||
|
emptyObjectIfMissing: true,
|
||||||
})
|
})
|
||||||
credential_mapping_overrides;
|
credential_mapping_overrides;
|
||||||
|
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ export default class GeneratedCredentialModel extends BaseModel {
|
|||||||
})
|
})
|
||||||
username;
|
username;
|
||||||
|
|
||||||
// =attributes (username_password, username_password_domain)
|
// =attributes (username_password, username_password_domain, password)
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
for: ['username_password', 'username_password_domain'],
|
for: ['username_password', 'username_password_domain', 'password'],
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
isSecret: true,
|
isSecret: true,
|
||||||
description: 'The password for credential.',
|
description: 'The password for credential.',
|
||||||
|
|||||||
@@ -49,7 +49,13 @@ export default class SqliteHandler {
|
|||||||
const schema = store.modelFor(type);
|
const schema = store.modelFor(type);
|
||||||
const serializer = store.serializerFor(type);
|
const serializer = store.serializerFor(type);
|
||||||
|
|
||||||
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
|
let {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
select,
|
||||||
|
query: queryObj,
|
||||||
|
...remainingQuery
|
||||||
|
} = query;
|
||||||
let payload,
|
let payload,
|
||||||
listToken,
|
listToken,
|
||||||
writeToDbPromise,
|
writeToDbPromise,
|
||||||
@@ -101,6 +107,7 @@ export default class SqliteHandler {
|
|||||||
payload,
|
payload,
|
||||||
storeToken,
|
storeToken,
|
||||||
tokenKey,
|
tokenKey,
|
||||||
|
pushToStore,
|
||||||
serializer,
|
serializer,
|
||||||
store,
|
store,
|
||||||
schema,
|
schema,
|
||||||
@@ -119,7 +126,7 @@ export default class SqliteHandler {
|
|||||||
const { sql, parameters } = generateSQLExpressions(type, queryObj, {
|
const { sql, parameters } = generateSQLExpressions(type, queryObj, {
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
select: ['data'],
|
select: select ?? [{ field: 'data' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const rows = await this.sqlite.fetchResource({
|
const rows = await this.sqlite.fetchResource({
|
||||||
@@ -129,7 +136,7 @@ export default class SqliteHandler {
|
|||||||
|
|
||||||
const { sql: countSql, parameters: countParams } =
|
const { sql: countSql, parameters: countParams } =
|
||||||
generateSQLExpressions(type, queryObj, {
|
generateSQLExpressions(type, queryObj, {
|
||||||
select: ['count(*) as total'],
|
select: [{ field: '*', isCount: true, alias: 'total' }],
|
||||||
});
|
});
|
||||||
const count = await this.sqlite.fetchResource({
|
const count = await this.sqlite.fetchResource({
|
||||||
sql: countSql,
|
sql: countSql,
|
||||||
@@ -207,6 +214,7 @@ export default class SqliteHandler {
|
|||||||
payload,
|
payload,
|
||||||
storeToken,
|
storeToken,
|
||||||
tokenKey,
|
tokenKey,
|
||||||
|
pushToStore,
|
||||||
serializer,
|
serializer,
|
||||||
store,
|
store,
|
||||||
schema,
|
schema,
|
||||||
@@ -218,11 +226,6 @@ export default class SqliteHandler {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(
|
const normalizedPayload = serializer.normalizeResponse(
|
||||||
store,
|
store,
|
||||||
schema,
|
schema,
|
||||||
@@ -233,6 +236,19 @@ export default class SqliteHandler {
|
|||||||
|
|
||||||
const { data: payloadData } = normalizedPayload;
|
const { data: payloadData } = normalizedPayload;
|
||||||
|
|
||||||
|
// Remove any records from the DB if the API indicates they've been deleted
|
||||||
|
// Additionally remove deleted records from ember data store for symmetry
|
||||||
|
if (payload.removed_ids?.length > 0) {
|
||||||
|
await this.sqlite.deleteResource(type, payload.removed_ids);
|
||||||
|
|
||||||
|
if (pushToStore) {
|
||||||
|
payload.removed_ids.forEach((id) => {
|
||||||
|
const record = store.peekRecord(type, id);
|
||||||
|
store.unloadRecord(record);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store the new data we just got back from the API refresh
|
// Store the new data we just got back from the API refresh
|
||||||
const items = payloadData.map((datum) => {
|
const items = payloadData.map((datum) => {
|
||||||
const params = Object.values(modelMapping[type]).map((key) => {
|
const params = Object.values(modelMapping[type]).map((key) => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||||
|
TYPE_CREDENTIAL_PASSWORD,
|
||||||
} from 'api/models/credential';
|
} from 'api/models/credential';
|
||||||
/**
|
/**
|
||||||
* Enum options for credential library.
|
* Enum options for credential library.
|
||||||
@@ -19,6 +20,7 @@ export const options = {
|
|||||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||||
|
TYPE_CREDENTIAL_PASSWORD,
|
||||||
],
|
],
|
||||||
mapping_overrides: {
|
mapping_overrides: {
|
||||||
username_password: ['username_attribute', 'password_attribute'],
|
username_password: ['username_attribute', 'password_attribute'],
|
||||||
@@ -32,6 +34,7 @@ export const options = {
|
|||||||
'password_attribute',
|
'password_attribute',
|
||||||
'domain_attribute',
|
'domain_attribute',
|
||||||
],
|
],
|
||||||
|
password: ['password_attribute'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,16 @@ import GeneratedCredentialModel from '../generated/models/credential';
|
|||||||
export const TYPE_CREDENTIAL_USERNAME_PASSWORD = 'username_password';
|
export const TYPE_CREDENTIAL_USERNAME_PASSWORD = 'username_password';
|
||||||
export const TYPE_CREDENTIAL_SSH_PRIVATE_KEY = 'ssh_private_key';
|
export const TYPE_CREDENTIAL_SSH_PRIVATE_KEY = 'ssh_private_key';
|
||||||
export const TYPE_CREDENTIAL_JSON = 'json';
|
export const TYPE_CREDENTIAL_JSON = 'json';
|
||||||
export const TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN = 'username_password_domain';
|
export const TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN =
|
||||||
|
'username_password_domain';
|
||||||
|
export const TYPE_CREDENTIAL_PASSWORD = 'password';
|
||||||
|
|
||||||
export const TYPES_CREDENTIAL = Object.freeze([
|
export const TYPES_CREDENTIAL = Object.freeze([
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||||
TYPE_CREDENTIAL_JSON,
|
TYPE_CREDENTIAL_JSON,
|
||||||
|
TYPE_CREDENTIAL_PASSWORD,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default class CredentialModel extends GeneratedCredentialModel {
|
export default class CredentialModel extends GeneratedCredentialModel {
|
||||||
|
|||||||
@@ -98,8 +98,9 @@ export default class AccountSerializer extends ApplicationSerializer {
|
|||||||
serialized.attributes = {};
|
serialized.attributes = {};
|
||||||
}
|
}
|
||||||
// New record case
|
// New record case
|
||||||
if (password && snapshot?.record?.isNew)
|
if (password && snapshot?.record?.isNew) {
|
||||||
serialized.attributes.password = password;
|
serialized.attributes.password = password;
|
||||||
|
}
|
||||||
// Set password custom method
|
// Set password custom method
|
||||||
if (snapshot?.adapterOptions?.method === 'set-password') {
|
if (snapshot?.adapterOptions?.method === 'set-password') {
|
||||||
serialized = this.serializeForSetPassword(snapshot, password);
|
serialized = this.serializeForSetPassword(snapshot, password);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import ApplicationSerializer from './application';
|
|||||||
import {
|
import {
|
||||||
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||||
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
||||||
TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
|
|
||||||
options,
|
options,
|
||||||
} from '../models/credential-library';
|
} from '../models/credential-library';
|
||||||
|
|
||||||
@@ -30,8 +29,6 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
|
|||||||
return this.serializeVaultGeneric(...arguments);
|
return this.serializeVaultGeneric(...arguments);
|
||||||
case TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE:
|
case TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE:
|
||||||
return this.serializeVaultSshCertificate(...arguments);
|
return this.serializeVaultSshCertificate(...arguments);
|
||||||
case TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP:
|
|
||||||
return this.serializeVaultLdap(...arguments);
|
|
||||||
default:
|
default:
|
||||||
return super.serialize(...arguments);
|
return super.serialize(...arguments);
|
||||||
}
|
}
|
||||||
@@ -64,15 +61,6 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
* Handle credential mapping overrides for serialization.
|
||||||
* @param {Object} serialized - The serialized object.
|
* @param {Object} serialized - The serialized object.
|
||||||
@@ -80,7 +68,9 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
|
|||||||
*/
|
*/
|
||||||
handleCredentialMappingOverrides(serialized, isNew) {
|
handleCredentialMappingOverrides(serialized, isNew) {
|
||||||
const { credential_type, credential_mapping_overrides } = serialized;
|
const { credential_type, credential_mapping_overrides } = serialized;
|
||||||
|
if (Object.keys(credential_mapping_overrides).length === 0) {
|
||||||
|
serialized.credential_mapping_overrides = null;
|
||||||
|
}
|
||||||
// API expects to send null to fields if it is undefined or deleted
|
// API expects to send null to fields if it is undefined or deleted
|
||||||
if (credential_mapping_overrides && !isNew) {
|
if (credential_mapping_overrides && !isNew) {
|
||||||
serialized.credential_mapping_overrides = options.mapping_overrides[
|
serialized.credential_mapping_overrides = options.mapping_overrides[
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ export default class CredentialStoreSerializer extends ApplicationSerializer {
|
|||||||
if (
|
if (
|
||||||
!serialized.attributes?.client_certificate_key &&
|
!serialized.attributes?.client_certificate_key &&
|
||||||
serialized.attributes?.client_certificate
|
serialized.attributes?.client_certificate
|
||||||
)
|
) {
|
||||||
delete serialized.attributes.client_certificate_key;
|
delete serialized.attributes.client_certificate_key;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return serialized;
|
return serialized;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,9 +76,10 @@ export default class StorageBucketSerializer extends ApplicationSerializer {
|
|||||||
// This deletes any fields that don't belong to the record's credential type
|
// This deletes any fields that don't belong to the record's credential type
|
||||||
if (options.isNestedAttribute && json.attributes) {
|
if (options.isNestedAttribute && json.attributes) {
|
||||||
// The key must be included in the fieldsByType list above
|
// The key must be included in the fieldsByType list above
|
||||||
if (!fieldsByCredentialType[credentialType].includes(key))
|
if (!fieldsByCredentialType[credentialType].includes(key)) {
|
||||||
// API requires these fields to be null
|
// API requires these fields to be null
|
||||||
json.attributes[key] = null;
|
json.attributes[key] = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//json.secrets is only present, if there are any updates to the secret fields
|
//json.secrets is only present, if there are any updates to the secret fields
|
||||||
|
|||||||
@@ -33,16 +33,18 @@ export default class TargetSerializer extends ApplicationSerializer {
|
|||||||
const injectedApplicationCredentialSourceIDs =
|
const injectedApplicationCredentialSourceIDs =
|
||||||
snapshot?.adapterOptions?.injectedApplicationCredentialSourceIDs;
|
snapshot?.adapterOptions?.injectedApplicationCredentialSourceIDs;
|
||||||
|
|
||||||
if (brokeredCredentialSourceIDs)
|
if (brokeredCredentialSourceIDs) {
|
||||||
serialized = this.serializeWithBrokeredCredentialSources(
|
serialized = this.serializeWithBrokeredCredentialSources(
|
||||||
snapshot,
|
snapshot,
|
||||||
brokeredCredentialSourceIDs,
|
brokeredCredentialSourceIDs,
|
||||||
);
|
);
|
||||||
if (injectedApplicationCredentialSourceIDs)
|
}
|
||||||
|
if (injectedApplicationCredentialSourceIDs) {
|
||||||
serialized = this.serializeWithInjectedApplicationCredentialSources(
|
serialized = this.serializeWithInjectedApplicationCredentialSources(
|
||||||
snapshot,
|
snapshot,
|
||||||
injectedApplicationCredentialSourceIDs,
|
injectedApplicationCredentialSourceIDs,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isNew && serialized?.with_aliases) {
|
if (isNew && serialized?.with_aliases) {
|
||||||
// API expects scope id along with every alias value
|
// API expects scope id along with every alias value
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ export default class UserSerializer extends ApplicationSerializer {
|
|||||||
serialize(snapshot) {
|
serialize(snapshot) {
|
||||||
let serialized = super.serialize(...arguments);
|
let serialized = super.serialize(...arguments);
|
||||||
const accountIDs = snapshot?.adapterOptions?.accountIDs;
|
const accountIDs = snapshot?.adapterOptions?.accountIDs;
|
||||||
if (accountIDs)
|
if (accountIDs) {
|
||||||
serialized = this.serializewithAccounts(snapshot, accountIDs);
|
serialized = this.serializewithAccounts(snapshot, accountIDs);
|
||||||
|
}
|
||||||
return serialized;
|
return serialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Service, { service } from '@ember/service';
|
||||||
|
import { modelMapping } from 'api/services/sqlite';
|
||||||
|
import { assert } from '@ember/debug';
|
||||||
|
import { generateSQLExpressions } from '../utils/sqlite-query';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database service for querying Boundary resources from storage.
|
||||||
|
* Provides a high-level interface for executing queries against the local database.
|
||||||
|
*/
|
||||||
|
export default class DbService extends Service {
|
||||||
|
@service sqlite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query resources from the database by type.
|
||||||
|
*
|
||||||
|
* @param {string} type - The resource type (e.g., 'scope', 'user', 'target').
|
||||||
|
* Must be a supported model type defined in modelMapping.
|
||||||
|
* @param {Object} query - Query configuration object
|
||||||
|
* @returns {Promise<Object>} Promise resolving to query results from sqlite service
|
||||||
|
* @throws {Error} Assertion error if resource type is not supported
|
||||||
|
*/
|
||||||
|
query(type, query) {
|
||||||
|
const supportedModels = Object.keys(modelMapping);
|
||||||
|
assert('Resource type is not supported.', supportedModels.includes(type));
|
||||||
|
|
||||||
|
let { page, pageSize, select, query: queryObj } = query;
|
||||||
|
|
||||||
|
const { sql, parameters } = generateSQLExpressions(type, queryObj, {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.sqlite.fetchResource({
|
||||||
|
sql,
|
||||||
|
parameters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,8 @@ export const modelMapping = {
|
|||||||
target_name: 'create_time_values.target.name',
|
target_name: 'create_time_values.target.name',
|
||||||
target_scope_id: 'create_time_values.target.scope.id',
|
target_scope_id: 'create_time_values.target.scope.id',
|
||||||
target_scope_name: 'create_time_values.target.scope.name',
|
target_scope_name: 'create_time_values.target.scope.name',
|
||||||
|
target_scope_parent_scope_id:
|
||||||
|
'create_time_values.target.scope.parent_scope_id',
|
||||||
created_time: 'created_time',
|
created_time: 'created_time',
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
@@ -114,21 +116,6 @@ export const modelMapping = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 {
|
export default class SqliteDbService extends Service {
|
||||||
// =attributes
|
// =attributes
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { modelMapping } from 'api/services/sqlite';
|
import { modelMapping } from 'api/services/sqlite';
|
||||||
import { searchTables } from 'api/services/sqlite';
|
|
||||||
import { typeOf } from '@ember/utils';
|
import { typeOf } from '@ember/utils';
|
||||||
import { underscore } from '@ember/string';
|
import { underscore } from '@ember/string';
|
||||||
|
import { assert } from '@ember/debug';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a POJO representing a filter query and builds a SQL query.
|
* Takes a POJO representing a filter query and builds a SQL query.
|
||||||
@@ -31,12 +31,14 @@ export function generateSQLExpressions(
|
|||||||
const conditions = [];
|
const conditions = [];
|
||||||
const parameters = [];
|
const parameters = [];
|
||||||
const tableName = underscore(resource);
|
const tableName = underscore(resource);
|
||||||
|
const joins = [];
|
||||||
|
|
||||||
addFilterConditions({ filters, parameters, conditions });
|
addFilterConditions({ filters, parameters, conditions, joins, tableName });
|
||||||
addSearchConditions({ search, resource, tableName, parameters, conditions });
|
addSearchConditions({ search, resource, tableName, parameters, conditions });
|
||||||
|
|
||||||
const orderByClause = constructOrderByClause(resource, sort);
|
const selectClause = constructSelectClause(select, tableName);
|
||||||
|
const joinClause = constructJoinClause(joins);
|
||||||
|
const orderByClause = constructOrderByClause(resource, tableName, sort);
|
||||||
const whereClause = conditions.length ? `WHERE ${and(conditions)}` : '';
|
const whereClause = conditions.length ? `WHERE ${and(conditions)}` : '';
|
||||||
|
|
||||||
const paginationClause = page && pageSize ? `LIMIT ? OFFSET ?` : '';
|
const paginationClause = page && pageSize ? `LIMIT ? OFFSET ?` : '';
|
||||||
@@ -44,13 +46,12 @@ export function generateSQLExpressions(
|
|||||||
parameters.push(pageSize, (page - 1) * pageSize);
|
parameters.push(pageSize, (page - 1) * pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectClause = `SELECT ${select ? select.join(', ') : '*'} FROM "${tableName}"`;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Replace any empty newlines or leading whitespace on each line to be consistent with formatting
|
// 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.
|
// This is mainly to help us read and test the generated SQL as it has no effect on the actual SQL execution.
|
||||||
sql: `
|
sql: `
|
||||||
${selectClause}
|
${selectClause}
|
||||||
|
${joinClause}
|
||||||
${whereClause}
|
${whereClause}
|
||||||
${orderByClause}
|
${orderByClause}
|
||||||
${paginationClause}`
|
${paginationClause}`
|
||||||
@@ -60,7 +61,13 @@ export function generateSQLExpressions(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function addFilterConditions({ filters, parameters, conditions }) {
|
function addFilterConditions({
|
||||||
|
filters,
|
||||||
|
parameters,
|
||||||
|
conditions,
|
||||||
|
joins,
|
||||||
|
tableName,
|
||||||
|
}) {
|
||||||
if (!filters) {
|
if (!filters) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -71,7 +78,7 @@ function addFilterConditions({ filters, parameters, conditions }) {
|
|||||||
? filterArrayOrObject
|
? filterArrayOrObject
|
||||||
: filterArrayOrObject.values;
|
: filterArrayOrObject.values;
|
||||||
|
|
||||||
if (!filterValueArray || !filterValueArray.length) {
|
if (!filterValueArray || !filterValueArray.length || key === 'joins') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +95,13 @@ function addFilterConditions({ filters, parameters, conditions }) {
|
|||||||
allOperatorsEqual
|
allOperatorsEqual
|
||||||
) {
|
) {
|
||||||
const operation = firstOperator === 'equals' ? 'in' : 'notIn';
|
const operation = firstOperator === 'equals' ? 'in' : 'notIn';
|
||||||
|
const nullOperator =
|
||||||
|
firstOperator === 'equals' ? 'IS NULL' : 'IS NOT NULL';
|
||||||
const values = filterValueArray
|
const values = filterValueArray
|
||||||
.filter((f) => f)
|
.filter(
|
||||||
|
(filterObjValue) =>
|
||||||
|
filterObjValue && Object.values(filterObjValue)[0] !== null,
|
||||||
|
)
|
||||||
.map((filterObjValue) => {
|
.map((filterObjValue) => {
|
||||||
let value = Object.values(filterObjValue)[0];
|
let value = Object.values(filterObjValue)[0];
|
||||||
if (typeOf(value) === 'date') {
|
if (typeOf(value) === 'date') {
|
||||||
@@ -98,7 +110,14 @@ function addFilterConditions({ filters, parameters, conditions }) {
|
|||||||
parameters.push(value);
|
parameters.push(value);
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
const filterCondition = `${key}${OPERATORS[operation](values)}`;
|
|
||||||
|
// Add a check for null values as an IN clause will mistakenly use `=` for null values.
|
||||||
|
// We'll add an extra OR clause to handle nulls separately.
|
||||||
|
const isNullValue = filterValueArray.some(
|
||||||
|
(filterObjValue) => Object.values(filterObjValue)[0] === null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterCondition = `"${tableName}".${key}${OPERATORS[operation](values)}${isNullValue ? ` OR "${tableName}".${key} ${nullOperator}` : ''}`;
|
||||||
conditions.push(parenthetical(filterCondition));
|
conditions.push(parenthetical(filterCondition));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -108,6 +127,16 @@ function addFilterConditions({ filters, parameters, conditions }) {
|
|||||||
.map((filterObjValue) => {
|
.map((filterObjValue) => {
|
||||||
let [operation, value] = Object.entries(filterObjValue)[0];
|
let [operation, value] = Object.entries(filterObjValue)[0];
|
||||||
|
|
||||||
|
// Handle null values: convert equals/notEquals to IS NULL or IS NOT NULL
|
||||||
|
if (value === null) {
|
||||||
|
if (operation === 'equals') {
|
||||||
|
return `"${tableName}".${key} IS NULL`;
|
||||||
|
}
|
||||||
|
if (operation === 'notEquals') {
|
||||||
|
return `"${tableName}".${key} IS NOT NULL`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SQLite needs to be working with ISO strings
|
// SQLite needs to be working with ISO strings
|
||||||
if (typeOf(value) === 'date') {
|
if (typeOf(value) === 'date') {
|
||||||
value = value.toISOString();
|
value = value.toISOString();
|
||||||
@@ -120,7 +149,7 @@ function addFilterConditions({ filters, parameters, conditions }) {
|
|||||||
parameters.push(value);
|
parameters.push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${key}${OPERATORS[operation]}`;
|
return `"${tableName}".${key}${OPERATORS[operation]}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { logicalOperator } = filterArrayOrObject;
|
const { logicalOperator } = filterArrayOrObject;
|
||||||
@@ -133,6 +162,43 @@ function addFilterConditions({ filters, parameters, conditions }) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filters.joins?.length > 0) {
|
||||||
|
const tableNameIndex = {};
|
||||||
|
|
||||||
|
filters.joins.forEach((join) => {
|
||||||
|
const {
|
||||||
|
resource,
|
||||||
|
query,
|
||||||
|
joinFrom = 'id',
|
||||||
|
joinOn,
|
||||||
|
joinType = 'INNER',
|
||||||
|
} = join;
|
||||||
|
const joinTableName = underscore(resource);
|
||||||
|
tableNameIndex[resource] ??= 1;
|
||||||
|
// Alias in the possible scenario we join the same table more than once
|
||||||
|
const alias = `${joinTableName}${tableNameIndex[resource]}`;
|
||||||
|
|
||||||
|
joins.push({
|
||||||
|
type: joinType,
|
||||||
|
table: joinTableName,
|
||||||
|
alias,
|
||||||
|
condition: `"${tableName}".${joinFrom} = ${alias}.${joinOn}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the conditions from the join query
|
||||||
|
if (query?.filters) {
|
||||||
|
addFilterConditions({
|
||||||
|
filters: query.filters,
|
||||||
|
parameters,
|
||||||
|
conditions,
|
||||||
|
tableName: alias,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tableNameIndex[resource]++;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSearchConditions({
|
function addSearchConditions({
|
||||||
@@ -142,39 +208,148 @@ function addSearchConditions({
|
|||||||
parameters,
|
parameters,
|
||||||
conditions,
|
conditions,
|
||||||
}) {
|
}) {
|
||||||
if (!search) {
|
if (!search || !modelMapping[resource]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const searchSelect = search?.select ?? 'rowid';
|
||||||
|
const searchSql = `SELECT ${searchSelect} FROM ${tableName}_fts WHERE ${tableName}_fts MATCH ?`;
|
||||||
|
const getParameter = (fields, text) =>
|
||||||
|
fields?.length > 0
|
||||||
|
? or(fields.map((field) => `${field}:"${text}"*`))
|
||||||
|
: `"${text}"*`;
|
||||||
|
|
||||||
if (searchTables.has(resource)) {
|
// Use the special prefix indicator "*" for full-text search
|
||||||
// Use the special prefix indicator "*" for full-text search
|
if (typeOf(search) === 'object') {
|
||||||
|
if (!search.text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameter = getParameter(search.fields, search.text);
|
||||||
|
parameters.push(parameter);
|
||||||
|
} else {
|
||||||
parameters.push(`"${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.
|
// If there are extra related searches on other tables, add them too
|
||||||
conditions.push(
|
if (search?.relatedSearches?.length > 0) {
|
||||||
`rowid IN (SELECT rowid FROM ${tableName}_fts WHERE ${tableName}_fts MATCH ?)`,
|
const relatedSearchQueries = [];
|
||||||
|
|
||||||
|
search.relatedSearches.forEach((relatedSearch) => {
|
||||||
|
const { resource: relatedResource, fields, join } = relatedSearch;
|
||||||
|
const relatedTableName = underscore(relatedResource);
|
||||||
|
|
||||||
|
const parameter = getParameter(fields, search.text);
|
||||||
|
parameters.push(parameter);
|
||||||
|
|
||||||
|
// Build the related FTS query with optional join
|
||||||
|
let relatedQuery = [];
|
||||||
|
relatedQuery.push(
|
||||||
|
`SELECT "${tableName}".${searchSelect} FROM ${relatedTableName}_fts`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (join) {
|
||||||
|
const { joinFrom = 'id', joinOn } = join;
|
||||||
|
relatedQuery.push(
|
||||||
|
`JOIN "${tableName}" ON "${tableName}".${joinFrom} = ${relatedResource}_fts.${joinOn}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
relatedQuery.push(`WHERE ${relatedTableName}_fts MATCH ?`);
|
||||||
|
|
||||||
|
relatedSearchQueries.push(relatedQuery.join(' '));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the original search first since we've already added the original parameter first
|
||||||
|
const conditionStatement = [searchSql, ...relatedSearchQueries].join(
|
||||||
|
'\nUNION ',
|
||||||
);
|
);
|
||||||
|
conditions.push(
|
||||||
|
`"${tableName}".${searchSelect} IN (${conditionStatement})`,
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = Object.keys(modelMapping[resource]);
|
// Use a subquery to match against the FTS table with rowids as SQLite is
|
||||||
const searchConditions = parenthetical(
|
// much more efficient with FTS queries when using rowids or MATCH (or both).
|
||||||
or(
|
// We could have also used a join here but a subquery is simpler.
|
||||||
fields.map((field) => {
|
conditions.push(`"${tableName}".${searchSelect} IN (${searchSql})`);
|
||||||
parameters.push(`%${search}%`);
|
|
||||||
return `${field}${OPERATORS['contains']}`;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
conditions.push(searchConditions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function constructOrderByClause(resource, sort) {
|
function constructSelectClause(select = [{ field: '*' }], tableName) {
|
||||||
const defaultOrderByClause = 'ORDER BY created_time DESC';
|
const distinctColumns = select.filter(({ isDistinct }) => isDistinct);
|
||||||
|
const nonDistinctColumns = select.filter(({ isDistinct }) => !isDistinct);
|
||||||
|
|
||||||
|
const buildColumnExpression = ({ field, isCount, alias, isDistinct }) => {
|
||||||
|
let column = field === '*' ? field : `"${tableName}".${field}`;
|
||||||
|
|
||||||
|
// If there's just distinct with nothing else, this will be handled separately
|
||||||
|
// and we will just return the column back as is
|
||||||
|
if (isCount && isDistinct) {
|
||||||
|
column = `count(DISTINCT ${column})`;
|
||||||
|
} else if (isCount) {
|
||||||
|
column = `count(${column})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alias) {
|
||||||
|
column = `${column} as ${alias}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return column;
|
||||||
|
};
|
||||||
|
|
||||||
|
let selectColumns;
|
||||||
|
|
||||||
|
if (distinctColumns.length > 0) {
|
||||||
|
// Check if any columns also have COUNT (or other aggregate function in the future)
|
||||||
|
const hasAggregateDistinct = distinctColumns.some((col) => col.isCount);
|
||||||
|
|
||||||
|
if (hasAggregateDistinct) {
|
||||||
|
selectColumns = distinctColumns.map(buildColumnExpression).join(', ');
|
||||||
|
|
||||||
|
// Add back in the non distinct columns
|
||||||
|
if (nonDistinctColumns.length > 0) {
|
||||||
|
const regularPart = nonDistinctColumns
|
||||||
|
.map(buildColumnExpression)
|
||||||
|
.join(', ');
|
||||||
|
selectColumns = `${selectColumns}, ${regularPart}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
'Can not combine non-distincts with multi column distincts',
|
||||||
|
nonDistinctColumns.length === 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only do multi column DISTINCT with no aggregate functions
|
||||||
|
selectColumns = `DISTINCT ${distinctColumns.map(buildColumnExpression).join(', ')}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectColumns = select.map(buildColumnExpression).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `SELECT ${selectColumns} FROM "${tableName}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructJoinClause(joins) {
|
||||||
|
if (!joins || joins.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return joins
|
||||||
|
.map(
|
||||||
|
({ type, table, alias, condition }) =>
|
||||||
|
`${type} JOIN "${table}" ${alias} ON ${condition}`,
|
||||||
|
)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructOrderByClause(resource, tableName, sort) {
|
||||||
|
const defaultOrderByClause = `ORDER BY "${tableName}".created_time DESC`;
|
||||||
|
|
||||||
const { attributes, customSort, direction, isCoalesced } = sort;
|
const { attributes, customSort, direction, isCoalesced } = sort;
|
||||||
const sortDirection = direction === 'desc' ? 'DESC' : 'ASC';
|
const sortDirection = direction === 'desc' ? 'DESC' : 'ASC';
|
||||||
|
const attributesWithTableName = attributes?.map(
|
||||||
|
(attribute) => `"${tableName}".${attribute}`,
|
||||||
|
);
|
||||||
|
|
||||||
// We have to check if the attributes are valid for the resource
|
// We have to check if the attributes are valid for the resource
|
||||||
// as we can't use parameterized queries for ORDER BY
|
// as we can't use parameterized queries for ORDER BY
|
||||||
@@ -190,21 +365,21 @@ function constructOrderByClause(resource, sort) {
|
|||||||
},
|
},
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
return `ORDER BY CASE ${attributes.join(', ')} ${whenClauses}END ${sortDirection}`;
|
return `ORDER BY CASE ${attributesWithTableName.join(', ')} ${whenClauses}END ${sortDirection}`;
|
||||||
} else if (attributes?.length > 0) {
|
} else if (attributes?.length > 0) {
|
||||||
const commaSeparatedVals = attributes.join(', ');
|
const commaSeparatedVals = attributesWithTableName.join(', ');
|
||||||
|
|
||||||
// In places where `collate nocase` is used, it is to ensure case is ignored on the initial sort.
|
// 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.
|
// Then, a sort on the same condition is performed to ensure upper-case strings are given preference in a tie.
|
||||||
if (isCoalesced) {
|
if (isCoalesced) {
|
||||||
return `ORDER BY COALESCE(${attributes.join(', ')}) COLLATE NOCASE ${sortDirection}, COALESCE(${commaSeparatedVals}) ${sortDirection}`;
|
return `ORDER BY COALESCE(${commaSeparatedVals}) COLLATE NOCASE ${sortDirection}, COALESCE(${commaSeparatedVals}) ${sortDirection}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributesWithNoCollate = attributes
|
const attributesWithNoCollate = attributes
|
||||||
.map((attr) => `${attr} COLLATE NOCASE ${sortDirection}`)
|
.map((attr) => `"${tableName}".${attr} COLLATE NOCASE ${sortDirection}`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
const attributesWithDirection = attributes
|
const attributesWithDirection = attributes
|
||||||
.map((attr) => `${attr} ${sortDirection}`)
|
.map((attr) => `"${tableName}".${attr} ${sortDirection}`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
return `ORDER BY ${attributesWithNoCollate}, ${attributesWithDirection}`;
|
return `ORDER BY ${attributesWithNoCollate}, ${attributesWithDirection}`;
|
||||||
} else if (modelMapping[resource]?.created_time) {
|
} else if (modelMapping[resource]?.created_time) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
// See "Maximum Number Of Host Parameters In A Single SQL Statement" in
|
// See "Maximum Number Of Host Parameters In A Single SQL Statement" in
|
||||||
// https://www.sqlite.org/limits.html
|
// https://www.sqlite.org/limits.html
|
||||||
const MAX_HOST_PARAMETERS = 32766;
|
const MAX_HOST_PARAMETERS = 32766;
|
||||||
const SCHEMA_VERSION = 1;
|
const SCHEMA_VERSION = 2;
|
||||||
|
|
||||||
// Some browsers do not allow calling getDirectory in private browsing modes even
|
// 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
|
// if we're in a secure context. This will cause the SQLite setup to fail so we should
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ const createTargetTables = `
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_target_scope_id_created_time ON target(scope_id, created_time DESC);
|
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.
|
-- Create a content FTS table that references the original table
|
||||||
-- Note that this only creates the FTS index and cannot reference the target content
|
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS target_fts USING fts5(
|
CREATE VIRTUAL TABLE IF NOT EXISTS target_fts USING fts5(
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
@@ -35,7 +34,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS target_fts USING fts5(
|
|||||||
address,
|
address,
|
||||||
scope_id,
|
scope_id,
|
||||||
created_time,
|
created_time,
|
||||||
content='',
|
content='target',
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create triggers to keep the FTS table in sync with the target table
|
-- Create triggers to keep the FTS table in sync with the target table
|
||||||
@@ -68,6 +67,7 @@ CREATE TABLE IF NOT EXISTS alias (
|
|||||||
data TEXT NOT NULL
|
data TEXT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_alias_created_time ON alias(created_time DESC);
|
CREATE INDEX IF NOT EXISTS idx_alias_created_time ON alias(created_time DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_alias_destination_id ON alias(destination_id);
|
||||||
|
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS alias_fts USING fts5(
|
CREATE VIRTUAL TABLE IF NOT EXISTS alias_fts USING fts5(
|
||||||
id,
|
id,
|
||||||
@@ -78,7 +78,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS alias_fts USING fts5(
|
|||||||
value,
|
value,
|
||||||
scope_id,
|
scope_id,
|
||||||
created_time,
|
created_time,
|
||||||
content='',
|
content='alias',
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS alias_ai AFTER INSERT ON alias BEGIN
|
CREATE TRIGGER IF NOT EXISTS alias_ai AFTER INSERT ON alias BEGIN
|
||||||
@@ -111,7 +111,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS group_fts USING fts5(
|
|||||||
description,
|
description,
|
||||||
scope_id,
|
scope_id,
|
||||||
created_time,
|
created_time,
|
||||||
content='',
|
content='group',
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS group_ai AFTER INSERT ON "group" BEGIN
|
CREATE TRIGGER IF NOT EXISTS group_ai AFTER INSERT ON "group" BEGIN
|
||||||
@@ -144,7 +144,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS role_fts USING fts5(
|
|||||||
description,
|
description,
|
||||||
scope_id,
|
scope_id,
|
||||||
created_time,
|
created_time,
|
||||||
content='',
|
content='role',
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS role_ai AFTER INSERT ON role BEGIN
|
CREATE TRIGGER IF NOT EXISTS role_ai AFTER INSERT ON role BEGIN
|
||||||
@@ -177,7 +177,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS user_fts USING fts5(
|
|||||||
description,
|
description,
|
||||||
scope_id,
|
scope_id,
|
||||||
created_time,
|
created_time,
|
||||||
content='',
|
content='user',
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS user_ai AFTER INSERT ON user BEGIN
|
CREATE TRIGGER IF NOT EXISTS user_ai AFTER INSERT ON user BEGIN
|
||||||
@@ -212,7 +212,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS credential_store_fts USING fts5(
|
|||||||
description,
|
description,
|
||||||
scope_id,
|
scope_id,
|
||||||
created_time,
|
created_time,
|
||||||
content='',
|
content='credential_store',
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS credential_store_ai AFTER INSERT ON credential_store BEGIN
|
CREATE TRIGGER IF NOT EXISTS credential_store_ai AFTER INSERT ON credential_store BEGIN
|
||||||
@@ -247,7 +247,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS scope_fts USING fts5(
|
|||||||
description,
|
description,
|
||||||
scope_id,
|
scope_id,
|
||||||
created_time,
|
created_time,
|
||||||
content='',
|
content='scope',
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS scope_ai AFTER INSERT ON scope BEGIN
|
CREATE TRIGGER IF NOT EXISTS scope_ai AFTER INSERT ON scope BEGIN
|
||||||
@@ -284,7 +284,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS auth_method_fts USING fts5(
|
|||||||
is_primary,
|
is_primary,
|
||||||
scope_id,
|
scope_id,
|
||||||
created_time,
|
created_time,
|
||||||
content='',
|
content='auth_method',
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS auth_method_ai AFTER INSERT ON auth_method BEGIN
|
CREATE TRIGGER IF NOT EXISTS auth_method_ai AFTER INSERT ON auth_method BEGIN
|
||||||
@@ -321,7 +321,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS host_catalog_fts USING fts5(
|
|||||||
plugin_name,
|
plugin_name,
|
||||||
scope_id,
|
scope_id,
|
||||||
created_time,
|
created_time,
|
||||||
content='',
|
content='host_catalog',
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS host_catalog_ai AFTER INSERT ON host_catalog BEGIN
|
CREATE TRIGGER IF NOT EXISTS host_catalog_ai AFTER INSERT ON host_catalog BEGIN
|
||||||
@@ -352,6 +352,7 @@ CREATE TABLE IF NOT EXISTS session_recording (
|
|||||||
target_name TEXT,
|
target_name TEXT,
|
||||||
target_scope_id TEXT,
|
target_scope_id TEXT,
|
||||||
target_scope_name TEXT,
|
target_scope_name TEXT,
|
||||||
|
target_scope_parent_scope_id TEXT,
|
||||||
created_time TEXT NOT NULL,
|
created_time TEXT NOT NULL,
|
||||||
data TEXT NOT NULL
|
data TEXT NOT NULL
|
||||||
);
|
);
|
||||||
@@ -371,21 +372,22 @@ CREATE VIRTUAL TABLE IF NOT EXISTS session_recording_fts USING fts5(
|
|||||||
target_name,
|
target_name,
|
||||||
target_scope_id,
|
target_scope_id,
|
||||||
target_scope_name,
|
target_scope_name,
|
||||||
|
target_scope_parent_scope_id,
|
||||||
created_time,
|
created_time,
|
||||||
content='',
|
content='session_recording',
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS session_recording_ai AFTER INSERT ON session_recording BEGIN
|
CREATE TRIGGER IF NOT EXISTS session_recording_ai AFTER INSERT ON session_recording BEGIN
|
||||||
INSERT INTO session_recording_fts(
|
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
|
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, target_scope_parent_scope_id, created_time
|
||||||
) VALUES (
|
) 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
|
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.target_scope_parent_scope_id, new.created_time
|
||||||
);
|
);
|
||||||
END;
|
END;
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS session_recording_ad AFTER DELETE ON session_recording BEGIN
|
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)
|
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, target_scope_parent_scope_id, 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);
|
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.target_scope_parent_scope_id, old.created_time);
|
||||||
END;`;
|
END;`;
|
||||||
|
|
||||||
const createSessionTables = `
|
const createSessionTables = `
|
||||||
@@ -411,7 +413,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS session_fts USING fts5(
|
|||||||
user_id,
|
user_id,
|
||||||
scope_id,
|
scope_id,
|
||||||
created_time,
|
created_time,
|
||||||
content='',
|
content='session',
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS session_ai AFTER INSERT ON session BEGIN
|
CREATE TRIGGER IF NOT EXISTS session_ai AFTER INSERT ON session BEGIN
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default } from 'api/services/db';
|
||||||
@@ -542,10 +542,11 @@ function routes() {
|
|||||||
session.update({
|
session.update({
|
||||||
status: pickRandomStatusString(),
|
status: pickRandomStatusString(),
|
||||||
});
|
});
|
||||||
if (session.id.split('-').pop() % 2)
|
if (session.id.split('-').pop() % 2) {
|
||||||
session.update({
|
session.update({
|
||||||
userId: 'authenticateduser',
|
userId: 'authenticateduser',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (recursive && scope_id === 'global') {
|
if (recursive && scope_id === 'global') {
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ export default factory.extend({
|
|||||||
if (this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC) {
|
if (this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC) {
|
||||||
return options.credential_types[i % options.credential_types.length];
|
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: () =>
|
authorized_actions: () =>
|
||||||
permissions.authorizedActionsFor('credential-library') || [
|
permissions.authorizedActionsFor('credential-library') || [
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default factory.extend({
|
|||||||
break;
|
break;
|
||||||
case 'static':
|
case 'static':
|
||||||
default:
|
default:
|
||||||
server.createList('credential', 3, {
|
server.createList('credential', 5, {
|
||||||
scope,
|
scope,
|
||||||
credentialStore,
|
credentialStore,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||||
|
TYPE_CREDENTIAL_JSON,
|
||||||
|
TYPE_CREDENTIAL_PASSWORD,
|
||||||
} from 'api/models/credential';
|
} from 'api/models/credential';
|
||||||
|
|
||||||
const types = [...TYPES_CREDENTIAL];
|
const types = [...TYPES_CREDENTIAL];
|
||||||
@@ -20,14 +22,16 @@ export default factory.extend({
|
|||||||
type: (i) => types[i % types.length],
|
type: (i) => types[i % types.length],
|
||||||
id() {
|
id() {
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case 'ssh_private_key':
|
case TYPE_CREDENTIAL_SSH_PRIVATE_KEY:
|
||||||
return generatedId('credspk_');
|
return generatedId('credspk_');
|
||||||
case 'username_password':
|
case TYPE_CREDENTIAL_USERNAME_PASSWORD:
|
||||||
return generatedId('credup_');
|
return generatedId('credup_');
|
||||||
case 'json':
|
case TYPE_CREDENTIAL_JSON:
|
||||||
return generatedId('credjson_');
|
return generatedId('credjson_');
|
||||||
case 'username_password_domain':
|
case TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN:
|
||||||
return generatedId('credupd_');
|
return generatedId('credupd_');
|
||||||
|
case TYPE_CREDENTIAL_PASSWORD:
|
||||||
|
return generatedId('credp_');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
authorized_actions: () =>
|
authorized_actions: () =>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { trait } from 'miragejs';
|
|||||||
import permissions from '../helpers/permissions';
|
import permissions from '../helpers/permissions';
|
||||||
import generateId from '../helpers/id';
|
import generateId from '../helpers/id';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { TYPE_AUTH_METHOD_PASSWORD } from 'api/models/auth-method';
|
||||||
|
|
||||||
export default factory.extend({
|
export default factory.extend({
|
||||||
type: 'global',
|
type: 'global',
|
||||||
@@ -108,4 +109,21 @@ export default factory.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
withGlobalAuth: trait({
|
||||||
|
afterCreate(record, server) {
|
||||||
|
if (record.type === 'global') {
|
||||||
|
const authMethod = server.create('auth-method', {
|
||||||
|
type: TYPE_AUTH_METHOD_PASSWORD,
|
||||||
|
scope: record,
|
||||||
|
});
|
||||||
|
server.create('account', {
|
||||||
|
type: TYPE_AUTH_METHOD_PASSWORD,
|
||||||
|
full_name: 'admin',
|
||||||
|
scope: record,
|
||||||
|
authMethod,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
@@ -17,7 +17,9 @@ import {
|
|||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||||
TYPE_CREDENTIAL_JSON,
|
TYPE_CREDENTIAL_JSON,
|
||||||
|
TYPE_CREDENTIAL_PASSWORD,
|
||||||
} from 'api/models/credential';
|
} from 'api/models/credential';
|
||||||
|
import { TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP } from 'api/models/credential-library';
|
||||||
|
|
||||||
const randomBoolean = (chance = 0.5) => Math.random() < chance;
|
const randomBoolean = (chance = 0.5) => Math.random() < chance;
|
||||||
const randomFilter = () =>
|
const randomFilter = () =>
|
||||||
@@ -111,7 +113,9 @@ export default factory.extend({
|
|||||||
server.schema.credentialLibraries,
|
server.schema.credentialLibraries,
|
||||||
(cred) =>
|
(cred) =>
|
||||||
cred.scopeId === scope.id &&
|
cred.scopeId === scope.id &&
|
||||||
cred.credential_type !== TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
cred.credential_type !== TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN &&
|
||||||
|
cred.credential_type !== TYPE_CREDENTIAL_PASSWORD &&
|
||||||
|
cred.type !== TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
|
||||||
);
|
);
|
||||||
const filteredCredentials = selectItems(
|
const filteredCredentials = selectItems(
|
||||||
server.schema.credentials,
|
server.schema.credentials,
|
||||||
@@ -119,6 +123,7 @@ export default factory.extend({
|
|||||||
cred.scopeId === scope.id &&
|
cred.scopeId === scope.id &&
|
||||||
![
|
![
|
||||||
TYPE_CREDENTIAL_JSON,
|
TYPE_CREDENTIAL_JSON,
|
||||||
|
TYPE_CREDENTIAL_PASSWORD,
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||||
].includes(cred.type),
|
].includes(cred.type),
|
||||||
);
|
);
|
||||||
@@ -127,11 +132,12 @@ export default factory.extend({
|
|||||||
const filteredCredentialLibrariesForRDP = selectItems(
|
const filteredCredentialLibrariesForRDP = selectItems(
|
||||||
server.schema.credentialLibraries,
|
server.schema.credentialLibraries,
|
||||||
(cred) =>
|
(cred) =>
|
||||||
cred.scopeId === scope.id &&
|
(cred.scopeId === scope.id &&
|
||||||
[
|
[
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||||
].includes(cred.credential_type),
|
].includes(cred.credential_type)) ||
|
||||||
|
cred.type == TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
|
||||||
);
|
);
|
||||||
const filteredCredentialsForRDP = selectItems(
|
const filteredCredentialsForRDP = selectItems(
|
||||||
server.schema.credentials,
|
server.schema.credentials,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const oidcRequiredAttempts = 3;
|
|||||||
|
|
||||||
const commandHandlers = {
|
const commandHandlers = {
|
||||||
password: {
|
password: {
|
||||||
login: (payload, scopeAttrs) => {
|
login: (payload, auth_method_id, account_id, scopeAttrs) => {
|
||||||
if (payload.attributes.login_name === 'error') {
|
if (payload.attributes.login_name === 'error') {
|
||||||
return new Response(400);
|
return new Response(400);
|
||||||
} else {
|
} else {
|
||||||
@@ -25,9 +25,9 @@ const commandHandlers = {
|
|||||||
scope: scopeAttrs,
|
scope: scopeAttrs,
|
||||||
id: 'token123',
|
id: 'token123',
|
||||||
token: 'thetokenstring',
|
token: 'thetokenstring',
|
||||||
account_id: 'authenticatedaccount',
|
account_id: account_id || 'authenticatedaccount',
|
||||||
user_id: 'authenticateduser',
|
user_id: 'authenticateduser',
|
||||||
auth_method_id: 'authmethod123',
|
auth_method_id: auth_method_id || 'authmethod123',
|
||||||
created_time: '',
|
created_time: '',
|
||||||
updated_time: '',
|
updated_time: '',
|
||||||
last_used_time: '',
|
last_used_time: '',
|
||||||
@@ -61,7 +61,7 @@ const commandHandlers = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
token: (_, scopeAttrs) => {
|
token: (_, auth_method_id, account_id, scopeAttrs) => {
|
||||||
oidcAttemptCounter++;
|
oidcAttemptCounter++;
|
||||||
if (oidcAttemptCounter < oidcRequiredAttempts) {
|
if (oidcAttemptCounter < oidcRequiredAttempts) {
|
||||||
return new Response(202);
|
return new Response(202);
|
||||||
@@ -74,9 +74,9 @@ const commandHandlers = {
|
|||||||
scope: scopeAttrs,
|
scope: scopeAttrs,
|
||||||
id: 'token123',
|
id: 'token123',
|
||||||
token: 'thetokenstring',
|
token: 'thetokenstring',
|
||||||
account_id: '1',
|
account_id: account_id || '1',
|
||||||
user_id: 'authenticateduser',
|
user_id: 'authenticateduser',
|
||||||
auth_method_id: 'authmethod123',
|
auth_method_id: auth_method_id || 'authmethod123',
|
||||||
created_time: '',
|
created_time: '',
|
||||||
updated_time: '',
|
updated_time: '',
|
||||||
last_used_time: '',
|
last_used_time: '',
|
||||||
@@ -90,7 +90,7 @@ const commandHandlers = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handles all custom methods on /auth-methods/:id route
|
// Handles all custom methods on /auth-methods/:id route
|
||||||
export function authHandler({ scopes, authMethods }, request) {
|
export function authHandler({ scopes, authMethods, accounts }, request) {
|
||||||
const [, id, method] = request.params.id_method.match(
|
const [, id, method] = request.params.id_method.match(
|
||||||
/(?<id>.[^:]*):(?<method>(.*))/,
|
/(?<id>.[^:]*):(?<method>(.*))/,
|
||||||
);
|
);
|
||||||
@@ -100,8 +100,14 @@ export function authHandler({ scopes, authMethods }, request) {
|
|||||||
const payload = JSON.parse(request.requestBody);
|
const payload = JSON.parse(request.requestBody);
|
||||||
const { command } = payload;
|
const { command } = payload;
|
||||||
const scope = scopes.find(authMethod.scopeId);
|
const scope = scopes.find(authMethod.scopeId);
|
||||||
|
const account = accounts.where({ authMethodId: authMethod.id })?.models[0];
|
||||||
const scopeAttrs = this.serialize(scopes.find(scope.id));
|
const scopeAttrs = this.serialize(scopes.find(scope.id));
|
||||||
return commandHandlers[authMethod.type][command](payload, scopeAttrs);
|
return commandHandlers[authMethod.type][command](
|
||||||
|
payload,
|
||||||
|
authMethod.id,
|
||||||
|
account?.id,
|
||||||
|
scopeAttrs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this handler doesn't really belong here, but we already route
|
// TODO: this handler doesn't really belong here, but we already route
|
||||||
|
|||||||
@@ -248,6 +248,20 @@ export default function initializeMockIPC(server, config) {
|
|||||||
resumeClientAgent() {}
|
resumeClientAgent() {}
|
||||||
hasRunningSessions() {}
|
hasRunningSessions() {}
|
||||||
stopAll() {}
|
stopAll() {}
|
||||||
|
getRdpClients() {
|
||||||
|
return ['windows-app', 'none', 'mstsc'];
|
||||||
|
}
|
||||||
|
getPreferredRdpClient() {
|
||||||
|
return 'windows-app';
|
||||||
|
}
|
||||||
|
getRecommendedRdpClient() {
|
||||||
|
return {
|
||||||
|
name: 'windows-app',
|
||||||
|
link: 'https://apps.apple.com/us/app/windows-app/id1295203466',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setPreferredRdpClient() {}
|
||||||
|
launchRdpClient() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ module('Unit | Serializer | credential library', function (hooks) {
|
|||||||
http_method: 'GET',
|
http_method: 'GET',
|
||||||
version: 1,
|
version: 1,
|
||||||
credential_type: 'ssh_private_key',
|
credential_type: 'ssh_private_key',
|
||||||
credential_mapping_overrides: [
|
credential_mapping_overrides: { username_attribute: 'user' },
|
||||||
{ key: 'username_attribute', value: 'user' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
const snapshot = record._createSnapshot();
|
const snapshot = record._createSnapshot();
|
||||||
const serializedRecord = serializer.serialize(snapshot);
|
const serializedRecord = serializer.serialize(snapshot);
|
||||||
@@ -64,9 +62,7 @@ module('Unit | Serializer | credential library', function (hooks) {
|
|||||||
http_method: 'GET',
|
http_method: 'GET',
|
||||||
version: 1,
|
version: 1,
|
||||||
credential_type: 'ssh_private_key',
|
credential_type: 'ssh_private_key',
|
||||||
credential_mapping_overrides: [
|
credential_mapping_overrides: { private_key_attribute: 'test' },
|
||||||
{ key: 'private_key_attribute', value: 'test' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -291,11 +287,6 @@ module('Unit | Serializer | credential library', function (hooks) {
|
|||||||
description: 'Description',
|
description: 'Description',
|
||||||
path: '/vault/path',
|
path: '/vault/path',
|
||||||
version: 1,
|
version: 1,
|
||||||
credential_type: 'username_password_domain',
|
|
||||||
credential_mapping_overrides: [
|
|
||||||
{ key: 'username_attribute', value: 'user' },
|
|
||||||
{ key: 'domain_attribute', value: 'domain' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
const snapshot = record._createSnapshot();
|
const snapshot = record._createSnapshot();
|
||||||
const serializedRecord = serializer.serialize(snapshot);
|
const serializedRecord = serializer.serialize(snapshot);
|
||||||
@@ -306,11 +297,6 @@ module('Unit | Serializer | credential library', function (hooks) {
|
|||||||
name: 'Name',
|
name: 'Name',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
version: 1,
|
version: 1,
|
||||||
credential_type: 'username_password_domain',
|
|
||||||
credential_mapping_overrides: {
|
|
||||||
username_attribute: 'user',
|
|
||||||
domain_attribute: 'domain',
|
|
||||||
},
|
|
||||||
attributes: {
|
attributes: {
|
||||||
path: '/vault/path',
|
path: '/vault/path',
|
||||||
},
|
},
|
||||||
@@ -326,15 +312,10 @@ module('Unit | Serializer | credential library', function (hooks) {
|
|||||||
type: 'credential-library',
|
type: 'credential-library',
|
||||||
attributes: {
|
attributes: {
|
||||||
type: TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
|
type: TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
|
||||||
name: 'Name',
|
name: 'Test',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
path: '/vault/path',
|
path: '/vault/path',
|
||||||
version: 1,
|
version: 1,
|
||||||
credential_type: 'username_password_domain',
|
|
||||||
credential_mapping_overrides: [
|
|
||||||
{ key: 'username_attribute', value: 'user' },
|
|
||||||
{ key: 'domain_attribute', value: 'domain' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -345,15 +326,9 @@ module('Unit | Serializer | credential library', function (hooks) {
|
|||||||
assert.deepEqual(serializedRecord, {
|
assert.deepEqual(serializedRecord, {
|
||||||
type: TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
|
type: TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
|
||||||
credential_store_id: null,
|
credential_store_id: null,
|
||||||
name: 'Name',
|
name: 'Test',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
version: 1,
|
version: 1,
|
||||||
credential_type: 'username_password_domain',
|
|
||||||
credential_mapping_overrides: {
|
|
||||||
username_attribute: 'user',
|
|
||||||
domain_attribute: 'domain',
|
|
||||||
password_attribute: null,
|
|
||||||
},
|
|
||||||
attributes: {
|
attributes: {
|
||||||
path: '/vault/path',
|
path: '/vault/path',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupTest } from 'dummy/tests/helpers';
|
||||||
|
|
||||||
|
module('Unit | Service | db', function (hooks) {
|
||||||
|
setupTest(hooks);
|
||||||
|
|
||||||
|
test('query throws assertion error for unsupported resource type', function (assert) {
|
||||||
|
let service = this.owner.lookup('service:db');
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
service.query('unsupported-type', { query: {} });
|
||||||
|
}, /Resource type is not supported/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,11 +6,10 @@
|
|||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupTest } from 'dummy/tests/helpers';
|
import { setupTest } from 'dummy/tests/helpers';
|
||||||
import { setupSqlite } from 'api/test-support/helpers/sqlite';
|
import { setupSqlite } from 'api/test-support/helpers/sqlite';
|
||||||
import { modelMapping, searchTables } from 'api/services/sqlite';
|
import { modelMapping } from 'api/services/sqlite';
|
||||||
import { underscore } from '@ember/string';
|
import { underscore } from '@ember/string';
|
||||||
|
|
||||||
const supportedModels = Object.keys(modelMapping);
|
const supportedModels = Object.keys(modelMapping);
|
||||||
const supportedFtsTables = [...searchTables];
|
|
||||||
|
|
||||||
module('Unit | Service | sqlite', function (hooks) {
|
module('Unit | Service | sqlite', function (hooks) {
|
||||||
setupTest(hooks);
|
setupTest(hooks);
|
||||||
@@ -38,7 +37,7 @@ module('Unit | Service | sqlite', function (hooks) {
|
|||||||
|
|
||||||
test.each(
|
test.each(
|
||||||
'Mapping matches fts table columns',
|
'Mapping matches fts table columns',
|
||||||
supportedFtsTables,
|
supportedModels,
|
||||||
async function (assert, resource) {
|
async function (assert, resource) {
|
||||||
const service = this.owner.lookup('service:sqlite');
|
const service = this.owner.lookup('service:sqlite');
|
||||||
|
|
||||||
|
|||||||
@@ -20,21 +20,6 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
delete String.prototype.removeExtraWhiteSpace;
|
delete String.prototype.removeExtraWhiteSpace;
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Add a normal LIKE search when we add a resource that uses it
|
|
||||||
test('it generates search correctly with FTS5', function (assert) {
|
|
||||||
const query = { search: 'favorite' };
|
|
||||||
|
|
||||||
const { sql, parameters } = generateSQLExpressions('target', query);
|
|
||||||
assert.strictEqual(
|
|
||||||
sql,
|
|
||||||
`
|
|
||||||
SELECT * FROM "target"
|
|
||||||
WHERE rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
|
||||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
|
||||||
);
|
|
||||||
assert.deepEqual(parameters, ['"favorite"*']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it grabs resources not in the model mapping', function (assert) {
|
test('it grabs resources not in the model mapping', function (assert) {
|
||||||
const query = {
|
const query = {
|
||||||
filters: { id: [{ equals: 'tokenKey' }] },
|
filters: { id: [{ equals: 'tokenKey' }] },
|
||||||
@@ -45,14 +30,14 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
sql,
|
sql,
|
||||||
`
|
`
|
||||||
SELECT * FROM "token"
|
SELECT * FROM "token"
|
||||||
WHERE (id = ?)`.removeExtraWhiteSpace(),
|
WHERE ("token".id = ?)`.removeExtraWhiteSpace(),
|
||||||
);
|
);
|
||||||
assert.deepEqual(parameters, ['tokenKey']);
|
assert.deepEqual(parameters, ['tokenKey']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it executes count queries correctly', function (assert) {
|
test('it executes count queries correctly', function (assert) {
|
||||||
const select = {
|
const select = {
|
||||||
select: ['count(*) as total'],
|
select: [{ field: '*', isCount: true, alias: 'total' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
const { sql, parameters } = generateSQLExpressions('target', {}, select);
|
const { sql, parameters } = generateSQLExpressions('target', {}, select);
|
||||||
@@ -60,11 +45,128 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
sql,
|
sql,
|
||||||
`
|
`
|
||||||
SELECT count(*) as total FROM "target"
|
SELECT count(*) as total FROM "target"
|
||||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
ORDER BY "target".created_time DESC`.removeExtraWhiteSpace(),
|
||||||
);
|
);
|
||||||
assert.deepEqual(parameters, []);
|
assert.deepEqual(parameters, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.each(
|
||||||
|
'it generates select clause with various options',
|
||||||
|
{
|
||||||
|
'single field': {
|
||||||
|
select: [{ field: 'name' }],
|
||||||
|
expectedSelect: '"target".name',
|
||||||
|
},
|
||||||
|
'multiple fields': {
|
||||||
|
select: [{ field: 'name' }, { field: 'type' }, { field: 'id' }],
|
||||||
|
expectedSelect: '"target".name, "target".type, "target".id',
|
||||||
|
},
|
||||||
|
'field with alias': {
|
||||||
|
select: [{ field: 'name', alias: 'resource_name' }],
|
||||||
|
expectedSelect: '"target".name as resource_name',
|
||||||
|
},
|
||||||
|
'multiple fields with aliases': {
|
||||||
|
select: [
|
||||||
|
{ field: 'name', alias: 'resource_name' },
|
||||||
|
{ field: 'type', alias: 'resource_type' },
|
||||||
|
],
|
||||||
|
expectedSelect: '"target".name as resource_name, "target".type as resource_type',
|
||||||
|
},
|
||||||
|
'count with alias': {
|
||||||
|
select: [{ field: 'id', isCount: true, alias: 'total_count' }],
|
||||||
|
expectedSelect: 'count("target".id) as total_count',
|
||||||
|
},
|
||||||
|
'count without alias': {
|
||||||
|
select: [{ field: '*', isCount: true }],
|
||||||
|
expectedSelect: 'count(*)',
|
||||||
|
},
|
||||||
|
'count distinct': {
|
||||||
|
select: [{ field: 'status', isCount: true, isDistinct: true }],
|
||||||
|
expectedSelect: 'count(DISTINCT "target".status)',
|
||||||
|
},
|
||||||
|
'count distinct with alias': {
|
||||||
|
select: [
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
isCount: true,
|
||||||
|
isDistinct: true,
|
||||||
|
alias: 'unique_types',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
expectedSelect: 'count(DISTINCT "target".type) as unique_types',
|
||||||
|
},
|
||||||
|
'mixed regular and count fields': {
|
||||||
|
select: [
|
||||||
|
{ field: 'name' },
|
||||||
|
{ field: 'id', isCount: true, alias: 'count' },
|
||||||
|
],
|
||||||
|
expectedSelect: '"target".name, count("target".id) as count',
|
||||||
|
},
|
||||||
|
'field with distinct': {
|
||||||
|
select: [{ field: 'type', isDistinct: true }],
|
||||||
|
expectedSelect: 'DISTINCT "target".type',
|
||||||
|
},
|
||||||
|
'multiple distinct fields': {
|
||||||
|
select: [
|
||||||
|
{ field: 'type', isDistinct: true },
|
||||||
|
{ field: 'status', isDistinct: true },
|
||||||
|
],
|
||||||
|
expectedSelect: 'DISTINCT "target".type, "target".status',
|
||||||
|
},
|
||||||
|
'multiple distinct with count fields': {
|
||||||
|
select: [
|
||||||
|
{ field: 'type', isDistinct: true, isCount: true },
|
||||||
|
{ field: 'status', isDistinct: true, isCount: true },
|
||||||
|
],
|
||||||
|
expectedSelect: 'count(DISTINCT "target".type), count(DISTINCT "target".status)',
|
||||||
|
},
|
||||||
|
'distinct with alias': {
|
||||||
|
select: [{ field: 'type', isDistinct: true, alias: 'unique_type' }],
|
||||||
|
expectedSelect: 'DISTINCT "target".type as unique_type',
|
||||||
|
},
|
||||||
|
'multiple distinct fields with aliases': {
|
||||||
|
select: [
|
||||||
|
{ field: 'type', isDistinct: true, alias: 'unique_type' },
|
||||||
|
{ field: 'status', isDistinct: true, alias: 'unique_status' },
|
||||||
|
],
|
||||||
|
expectedSelect: 'DISTINCT "target".type as unique_type, "target".status as unique_status',
|
||||||
|
},
|
||||||
|
'count distinct multiple fields': {
|
||||||
|
select: [
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
isCount: true,
|
||||||
|
isDistinct: true,
|
||||||
|
alias: 'type_count',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
isCount: true,
|
||||||
|
isDistinct: true,
|
||||||
|
alias: 'status_count',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
expectedSelect:
|
||||||
|
'count(DISTINCT "target".type) as type_count, count(DISTINCT "target".status) as status_count',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
function (assert, { select, expectedSelect }) {
|
||||||
|
const { sql, parameters } = generateSQLExpressions(
|
||||||
|
'target',
|
||||||
|
{},
|
||||||
|
{ select },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
sql,
|
||||||
|
`
|
||||||
|
SELECT ${expectedSelect} FROM "target"
|
||||||
|
ORDER BY "target".created_time DESC`.removeExtraWhiteSpace(),
|
||||||
|
);
|
||||||
|
assert.deepEqual(parameters, []);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test.each(
|
test.each(
|
||||||
'it generates filters correctly',
|
'it generates filters correctly',
|
||||||
{
|
{
|
||||||
@@ -74,7 +176,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
type: [{ equals: 'ssh' }],
|
type: [{ equals: 'ssh' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedWhereClause: 'WHERE (type = ?)',
|
expectedWhereClause: 'WHERE ("target".type = ?)',
|
||||||
expectedParams: ['ssh'],
|
expectedParams: ['ssh'],
|
||||||
},
|
},
|
||||||
notEquals: {
|
notEquals: {
|
||||||
@@ -83,7 +185,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
type: [{ notEquals: 'ssh' }],
|
type: [{ notEquals: 'ssh' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedWhereClause: 'WHERE (type != ?)',
|
expectedWhereClause: 'WHERE ("target".type != ?)',
|
||||||
expectedParams: ['ssh'],
|
expectedParams: ['ssh'],
|
||||||
},
|
},
|
||||||
contains: {
|
contains: {
|
||||||
@@ -92,7 +194,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
type: [{ contains: 'ssh' }],
|
type: [{ contains: 'ssh' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedWhereClause: 'WHERE (type LIKE ?)',
|
expectedWhereClause: 'WHERE ("target".type LIKE ?)',
|
||||||
expectedParams: ['%ssh%'],
|
expectedParams: ['%ssh%'],
|
||||||
},
|
},
|
||||||
greaterThan: {
|
greaterThan: {
|
||||||
@@ -102,7 +204,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
numberField: [{ gt: 10 }],
|
numberField: [{ gt: 10 }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedWhereClause: 'WHERE (created_time > ?) AND (numberField > ?)',
|
expectedWhereClause:
|
||||||
|
'WHERE ("target".created_time > ?) AND ("target".numberField > ?)',
|
||||||
expectedParams: [isoDateString, 10],
|
expectedParams: [isoDateString, 10],
|
||||||
},
|
},
|
||||||
lessThan: {
|
lessThan: {
|
||||||
@@ -112,7 +215,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
numberField: [{ lt: 10 }],
|
numberField: [{ lt: 10 }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedWhereClause: 'WHERE (created_time < ?) AND (numberField < ?)',
|
expectedWhereClause:
|
||||||
|
'WHERE ("target".created_time < ?) AND ("target".numberField < ?)',
|
||||||
expectedParams: [isoDateString, 10],
|
expectedParams: [isoDateString, 10],
|
||||||
},
|
},
|
||||||
greaterThanOrEqual: {
|
greaterThanOrEqual: {
|
||||||
@@ -122,7 +226,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
numberField: [{ gte: 10 }],
|
numberField: [{ gte: 10 }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedWhereClause: 'WHERE (created_time >= ?) AND (numberField >= ?)',
|
expectedWhereClause:
|
||||||
|
'WHERE ("target".created_time >= ?) AND ("target".numberField >= ?)',
|
||||||
expectedParams: [isoDateString, 10],
|
expectedParams: [isoDateString, 10],
|
||||||
},
|
},
|
||||||
lessThanOrEqual: {
|
lessThanOrEqual: {
|
||||||
@@ -132,7 +237,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
numberField: [{ lte: 10 }],
|
numberField: [{ lte: 10 }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedWhereClause: 'WHERE (created_time <= ?) AND (numberField <= ?)',
|
expectedWhereClause:
|
||||||
|
'WHERE ("target".created_time <= ?) AND ("target".numberField <= ?)',
|
||||||
expectedParams: [isoDateString, 10],
|
expectedParams: [isoDateString, 10],
|
||||||
},
|
},
|
||||||
logicalOperators: {
|
logicalOperators: {
|
||||||
@@ -150,9 +256,42 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedWhereClause:
|
expectedWhereClause:
|
||||||
'WHERE (id NOT IN (?, ?)) AND (status IN (?, ?)) AND (type = ?)',
|
'WHERE ("target".id NOT IN (?, ?)) AND ("target".status IN (?, ?)) AND ("target".type = ?)',
|
||||||
expectedParams: ['id1', 'id2', 'active', 'pending', 'ssh'],
|
expectedParams: ['id1', 'id2', 'active', 'pending', 'ssh'],
|
||||||
},
|
},
|
||||||
|
equalsNull: {
|
||||||
|
query: {
|
||||||
|
filters: {
|
||||||
|
description: [{ equals: null }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedWhereClause: 'WHERE ("target".description IS NULL)',
|
||||||
|
expectedParams: [],
|
||||||
|
},
|
||||||
|
notEqualsNull: {
|
||||||
|
query: {
|
||||||
|
filters: {
|
||||||
|
description: [{ notEquals: null }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedWhereClause: 'WHERE ("target".description IS NOT NULL)',
|
||||||
|
expectedParams: [],
|
||||||
|
},
|
||||||
|
mixedNullAndValues: {
|
||||||
|
query: {
|
||||||
|
filters: {
|
||||||
|
status: [
|
||||||
|
{ equals: 'active' },
|
||||||
|
{ equals: 'pending' },
|
||||||
|
{ equals: null },
|
||||||
|
],
|
||||||
|
type: [{ notEquals: null }, { notEquals: 'ssh' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedWhereClause:
|
||||||
|
'WHERE ("target".status IN (?, ?) OR "target".status IS NULL) AND ("target".type NOT IN (?) OR "target".type IS NOT NULL)',
|
||||||
|
expectedParams: ['active', 'pending', 'ssh'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
function (assert, { query, expectedWhereClause, expectedParams }) {
|
function (assert, { query, expectedWhereClause, expectedParams }) {
|
||||||
@@ -162,7 +301,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
`
|
`
|
||||||
SELECT * FROM "target"
|
SELECT * FROM "target"
|
||||||
${expectedWhereClause}
|
${expectedWhereClause}
|
||||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
ORDER BY "target".created_time DESC`.removeExtraWhiteSpace(),
|
||||||
);
|
);
|
||||||
assert.deepEqual(parameters, expectedParams);
|
assert.deepEqual(parameters, expectedParams);
|
||||||
},
|
},
|
||||||
@@ -184,7 +323,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
sql,
|
sql,
|
||||||
`
|
`
|
||||||
SELECT * FROM "target"
|
SELECT * FROM "target"
|
||||||
ORDER BY name COLLATE NOCASE ${expectedDirection}, name ${expectedDirection}`.removeExtraWhiteSpace(),
|
ORDER BY "target".name COLLATE NOCASE ${expectedDirection}, "target".name ${expectedDirection}`.removeExtraWhiteSpace(),
|
||||||
);
|
);
|
||||||
assert.deepEqual(parameters, []);
|
assert.deepEqual(parameters, []);
|
||||||
},
|
},
|
||||||
@@ -198,20 +337,20 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
attributes: ['name', 'id'],
|
attributes: ['name', 'id'],
|
||||||
isCoalesced: true,
|
isCoalesced: true,
|
||||||
},
|
},
|
||||||
expectedOrderByClause: `ORDER BY COALESCE(name, id) COLLATE NOCASE DESC, COALESCE(name, id) DESC`,
|
expectedOrderByClause: `ORDER BY COALESCE("target".name, "target".id) COLLATE NOCASE DESC, COALESCE("target".name, "target".id) DESC`,
|
||||||
},
|
},
|
||||||
'sort on multiple attributes': {
|
'sort on multiple attributes': {
|
||||||
sort: {
|
sort: {
|
||||||
attributes: ['name', 'id'],
|
attributes: ['name', 'id'],
|
||||||
},
|
},
|
||||||
expectedOrderByClause: `ORDER BY name COLLATE NOCASE DESC, id COLLATE NOCASE DESC, name DESC, id DESC`,
|
expectedOrderByClause: `ORDER BY "target".name COLLATE NOCASE DESC, "target".id COLLATE NOCASE DESC, "target".name DESC, "target".id DESC`,
|
||||||
},
|
},
|
||||||
'sort on mapped attributes': {
|
'sort on mapped attributes': {
|
||||||
sort: {
|
sort: {
|
||||||
attributes: ['type'],
|
attributes: ['type'],
|
||||||
customSort: { attributeMap: { ssh: 'SSH', tcp: 'Generic TCP' } },
|
customSort: { attributeMap: { ssh: 'SSH', tcp: 'Generic TCP' } },
|
||||||
},
|
},
|
||||||
expectedOrderByClause: `ORDER BY CASE type WHEN 'ssh' THEN 'SSH' WHEN 'tcp' THEN 'Generic TCP' END DESC`,
|
expectedOrderByClause: `ORDER BY CASE "target".type WHEN 'ssh' THEN 'SSH' WHEN 'tcp' THEN 'Generic TCP' END DESC`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
function (assert, { sort, expectedOrderByClause }) {
|
function (assert, { sort, expectedOrderByClause }) {
|
||||||
@@ -247,41 +386,189 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
sql,
|
sql,
|
||||||
`
|
`
|
||||||
SELECT * FROM "target"
|
SELECT * FROM "target"
|
||||||
ORDER BY created_time DESC
|
ORDER BY "target".created_time DESC
|
||||||
LIMIT ? OFFSET ?`.removeExtraWhiteSpace(),
|
LIMIT ? OFFSET ?`.removeExtraWhiteSpace(),
|
||||||
);
|
);
|
||||||
assert.deepEqual(parameters, expectedParams);
|
assert.deepEqual(parameters, expectedParams);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test('it generates FTS5 search with filters', function (assert) {
|
test.each(
|
||||||
const query = {
|
'it generates FTS5 search correctly',
|
||||||
search: 'favorite',
|
{
|
||||||
filters: {
|
'string search with filters': {
|
||||||
id: {
|
query: {
|
||||||
logicalOperator: 'and',
|
search: 'favorite',
|
||||||
values: [{ notEquals: 'id1' }, { notEquals: 'id2' }],
|
|
||||||
},
|
},
|
||||||
status: [{ equals: 'active' }, { equals: 'pending' }],
|
expectedSql: `
|
||||||
|
SELECT * FROM "target"
|
||||||
|
WHERE "target".rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
||||||
|
ORDER BY "target".created_time DESC`,
|
||||||
|
expectedParams: ['"favorite"*'],
|
||||||
},
|
},
|
||||||
};
|
'object search with field-specific searches': {
|
||||||
|
query: {
|
||||||
|
search: {
|
||||||
|
text: 'favorite',
|
||||||
|
fields: ['name', 'description'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSql: `
|
||||||
|
SELECT * FROM "target"
|
||||||
|
WHERE "target".rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
||||||
|
ORDER BY "target".created_time DESC`,
|
||||||
|
expectedParams: ['name:"favorite"* OR description:"favorite"*'],
|
||||||
|
},
|
||||||
|
'search with related searches': {
|
||||||
|
query: {
|
||||||
|
search: {
|
||||||
|
text: 'dev',
|
||||||
|
select: 'id',
|
||||||
|
relatedSearches: [
|
||||||
|
{
|
||||||
|
resource: 'alias',
|
||||||
|
fields: ['name', 'description'],
|
||||||
|
join: {
|
||||||
|
joinOn: 'destination_id',
|
||||||
|
joinFrom: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSql: `
|
||||||
|
SELECT * FROM "target"
|
||||||
|
WHERE "target".id IN (SELECT id FROM target_fts WHERE target_fts MATCH ?
|
||||||
|
UNION SELECT "target".id FROM alias_fts JOIN "target" ON "target".id = alias_fts.destination_id WHERE alias_fts MATCH ?)
|
||||||
|
ORDER BY "target".created_time DESC`,
|
||||||
|
expectedParams: ['"dev"*', 'name:"dev"* OR description:"dev"*'],
|
||||||
|
},
|
||||||
|
'search with multiple related searches': {
|
||||||
|
query: {
|
||||||
|
search: {
|
||||||
|
text: 'dev',
|
||||||
|
relatedSearches: [
|
||||||
|
{
|
||||||
|
resource: 'alias',
|
||||||
|
fields: ['name', 'description'],
|
||||||
|
join: {
|
||||||
|
joinOn: 'destination_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resource: 'session',
|
||||||
|
fields: ['name'],
|
||||||
|
join: {
|
||||||
|
joinOn: 'target_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSql: `
|
||||||
|
SELECT * FROM "target"
|
||||||
|
WHERE "target".rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?
|
||||||
|
UNION SELECT "target".rowid FROM alias_fts JOIN "target" ON "target".id = alias_fts.destination_id WHERE alias_fts MATCH ?
|
||||||
|
UNION SELECT "target".rowid FROM session_fts JOIN "target" ON "target".id = session_fts.target_id WHERE session_fts MATCH ?)
|
||||||
|
ORDER BY "target".created_time DESC`,
|
||||||
|
expectedParams: [
|
||||||
|
'"dev"*',
|
||||||
|
'name:"dev"* OR description:"dev"*',
|
||||||
|
'name:"dev"*',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
function (assert, { query, expectedSql, expectedParams }) {
|
||||||
|
const { sql, parameters } = generateSQLExpressions('target', query);
|
||||||
|
assert.strictEqual(sql, expectedSql.removeExtraWhiteSpace());
|
||||||
|
assert.deepEqual(parameters, expectedParams);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { sql, parameters } = generateSQLExpressions('target', query);
|
test.each(
|
||||||
assert.strictEqual(
|
'it generates joins with filters',
|
||||||
sql,
|
{
|
||||||
`
|
'basic joins with filters': {
|
||||||
SELECT * FROM "target"
|
query: {
|
||||||
WHERE (id NOT IN (?, ?)) AND (status IN (?, ?)) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
filters: {
|
||||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
type: [{ equals: 'ssh' }],
|
||||||
);
|
joins: [
|
||||||
assert.deepEqual(parameters, [
|
{
|
||||||
'id1',
|
resource: 'session',
|
||||||
'id2',
|
query: {
|
||||||
'active',
|
filters: {
|
||||||
'pending',
|
status: [{ equals: 'active' }],
|
||||||
'"favorite"*',
|
},
|
||||||
]);
|
},
|
||||||
});
|
joinOn: 'target_id',
|
||||||
|
joinType: 'INNER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resource: 'session-recording',
|
||||||
|
query: {
|
||||||
|
filters: {
|
||||||
|
state: [{ equals: 'available' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
joinFrom: 'idx',
|
||||||
|
joinOn: 'target_id',
|
||||||
|
joinType: 'LEFT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: [{ field: 'data' }],
|
||||||
|
expectedSql: `
|
||||||
|
SELECT "target".data FROM "target"
|
||||||
|
INNER JOIN "session" session1 ON "target".id = session1.target_id LEFT JOIN "session_recording" session_recording1 ON "target".idx = session_recording1.target_id
|
||||||
|
WHERE ("target".type = ?) AND ("session1".status = ?) AND ("session_recording1".state = ?)
|
||||||
|
ORDER BY "target".created_time DESC`,
|
||||||
|
expectedParams: ['ssh', 'active', 'available'],
|
||||||
|
},
|
||||||
|
'multiple joins same table': {
|
||||||
|
query: {
|
||||||
|
filters: {
|
||||||
|
joins: [
|
||||||
|
{
|
||||||
|
resource: 'session',
|
||||||
|
query: {
|
||||||
|
filters: {
|
||||||
|
status: [{ equals: 'active' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
joinOn: 'target_id',
|
||||||
|
joinType: 'INNER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resource: 'session',
|
||||||
|
query: {
|
||||||
|
filters: {
|
||||||
|
status: [{ equals: 'pending' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
joinOn: 'host_id',
|
||||||
|
joinType: 'LEFT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: [{ field: 'id' }],
|
||||||
|
expectedSql: `
|
||||||
|
SELECT "target".id FROM "target"
|
||||||
|
INNER JOIN "session" session1 ON "target".id = session1.target_id LEFT JOIN "session" session2 ON "target".id = session2.host_id
|
||||||
|
WHERE ("session1".status = ?) AND ("session2".status = ?)
|
||||||
|
ORDER BY "target".created_time DESC`,
|
||||||
|
expectedParams: ['active', 'pending'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
function (assert, { query, select, expectedSql, expectedParams }) {
|
||||||
|
const { sql, parameters } = generateSQLExpressions('target', query, {
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
assert.strictEqual(sql, expectedSql.removeExtraWhiteSpace());
|
||||||
|
assert.deepEqual(parameters, expectedParams);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('it generates SQL with all clauses combined', function (assert) {
|
test('it generates SQL with all clauses combined', function (assert) {
|
||||||
const query = {
|
const query = {
|
||||||
@@ -300,15 +587,15 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
const { sql, parameters } = generateSQLExpressions('target', query, {
|
const { sql, parameters } = generateSQLExpressions('target', query, {
|
||||||
page: 2,
|
page: 2,
|
||||||
pageSize: 15,
|
pageSize: 15,
|
||||||
select: ['data'],
|
select: [{ field: 'data' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
sql,
|
sql,
|
||||||
`
|
`
|
||||||
SELECT data FROM "target"
|
SELECT "target".data FROM "target"
|
||||||
WHERE (type = ?) AND (status IN (?, ?)) AND (created_time >= ?) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
WHERE ("target".type = ?) AND ("target".status IN (?, ?)) AND ("target".created_time >= ?) AND "target".rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
||||||
ORDER BY name COLLATE NOCASE DESC, name DESC
|
ORDER BY "target".name COLLATE NOCASE DESC, "target".name DESC
|
||||||
LIMIT ? OFFSET ?`.removeExtraWhiteSpace(),
|
LIMIT ? OFFSET ?`.removeExtraWhiteSpace(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -323,6 +610,35 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.each(
|
||||||
|
'it throws assertion error for invalid select combinations',
|
||||||
|
{
|
||||||
|
'mixing distinct with non-distinct columns': {
|
||||||
|
select: [
|
||||||
|
{ field: 'type', isDistinct: true },
|
||||||
|
{ field: 'status', isDistinct: true },
|
||||||
|
{ field: 'name' },
|
||||||
|
],
|
||||||
|
expectedError:
|
||||||
|
/Can not combine non-distincts with multi column distincts/,
|
||||||
|
},
|
||||||
|
'mixing distinct with aggregate non-distinct columns': {
|
||||||
|
select: [
|
||||||
|
{ field: 'type', isDistinct: true },
|
||||||
|
{ field: 'status', isDistinct: true },
|
||||||
|
{ field: 'name', isCount: true },
|
||||||
|
],
|
||||||
|
expectedError:
|
||||||
|
/Can not combine non-distincts with multi column distincts/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
function (assert, { select, expectedError }) {
|
||||||
|
assert.throws(() => {
|
||||||
|
generateSQLExpressions('target', {}, { select });
|
||||||
|
}, expectedError);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test.each(
|
test.each(
|
||||||
'it handles empty and invalid fields',
|
'it handles empty and invalid fields',
|
||||||
{
|
{
|
||||||
@@ -350,7 +666,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
|||||||
targetSql,
|
targetSql,
|
||||||
`
|
`
|
||||||
SELECT * FROM "target"
|
SELECT * FROM "target"
|
||||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
ORDER BY "target".created_time DESC`.removeExtraWhiteSpace(),
|
||||||
);
|
);
|
||||||
assert.deepEqual(targetParameters, []);
|
assert.deepEqual(targetParameters, []);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ module.exports = {
|
|||||||
browser: true,
|
browser: true,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
curly: ['error', 'multi-line', 'consistent'],
|
||||||
'ember/no-get': 'off',
|
'ember/no-get': 'off',
|
||||||
'ember/no-get-with-default': 'off',
|
'ember/no-get-with-default': 'off',
|
||||||
'ember/no-computed-properties-in-native-classes': 'off',
|
'ember/no-computed-properties-in-native-classes': 'off',
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
|||||||
* @param {string} username
|
* @param {string} username
|
||||||
* @return {object}
|
* @return {object}
|
||||||
*/
|
*/
|
||||||
normalizeData(data, username) {
|
normalizeData(data) {
|
||||||
// Pull fields up from `data.attributes` for easier access in JavaScript.
|
// Pull fields up from `data.attributes` for easier access in JavaScript.
|
||||||
// The `attributes` field exists on the Go side for its convenience but is
|
// The `attributes` field exists on the Go side for its convenience but is
|
||||||
// unnecessary here.
|
// unnecessary here.
|
||||||
@@ -109,7 +109,6 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
|||||||
// Add booleans indicated the scope type
|
// Add booleans indicated the scope type
|
||||||
data.isGlobal = data?.scope?.type === 'global';
|
data.isGlobal = data?.scope?.type === 'global';
|
||||||
data.isOrg = data?.scope?.type === 'org';
|
data.isOrg = data?.scope?.type === 'org';
|
||||||
if (username) data.username = username;
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,8 +72,6 @@ export default class PasswordAuthenticator extends BaseAuthenticator {
|
|||||||
fetch(authEndpointURL, { method: 'post', body }),
|
fetch(authEndpointURL, { method: 'post', body }),
|
||||||
);
|
);
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
return response.status < 400
|
return response.status < 400 ? resolve(this.normalizeData(json)) : reject();
|
||||||
? resolve(this.normalizeData(json, login_name))
|
|
||||||
: reject();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ module.exports = {
|
|||||||
browser: true,
|
browser: true,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
curly: ['error', 'multi-line', 'consistent'],
|
||||||
'ember/no-get': 'off',
|
'ember/no-get': 'off',
|
||||||
'ember/no-get-with-default': 'off',
|
'ember/no-get-with-default': 'off',
|
||||||
'ember/no-computed-properties-in-native-classes': 'off',
|
'ember/no-computed-properties-in-native-classes': 'off',
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<DD.Header @hasDivider={{true}}>
|
<DD.Header @hasDivider={{true}}>
|
||||||
<Hds::Form::TextInput::Base
|
<Hds::Form::TextInput::Base
|
||||||
@type='search'
|
@type='search'
|
||||||
@value={{this.searchTerm}}
|
@value={{or this.searchTerm @searchTerm}}
|
||||||
placeholder={{t 'actions.narrow-results'}}
|
placeholder={{t 'actions.narrow-results'}}
|
||||||
aria-label={{t 'actions.narrow-results'}}
|
aria-label={{t 'actions.narrow-results'}}
|
||||||
{{on 'input' this.filterItems}}
|
{{on 'input' this.filterItems}}
|
||||||
@@ -28,11 +28,18 @@
|
|||||||
/>
|
/>
|
||||||
</DD.Header>
|
</DD.Header>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless this.itemOptions}}
|
|
||||||
|
{{#unless (or this.itemOptions @isLoading)}}
|
||||||
<DD.Description @text={{t 'titles.no-results'}} />
|
<DD.Description @text={{t 'titles.no-results'}} />
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
{{#if (has-block)}}
|
{{#if @isLoading}}
|
||||||
|
<DD.Generic>
|
||||||
|
<Hds::Layout::Flex @align='center' @justify='center'>
|
||||||
|
<Hds::Icon @name='loading' @size='24' class='margin-y-xs' />
|
||||||
|
</Hds::Layout::Flex>
|
||||||
|
</DD.Generic>
|
||||||
|
{{else if (has-block)}}
|
||||||
{{yield DD this.selectItem this.itemOptions}}
|
{{yield DD this.selectItem this.itemOptions}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#each this.itemOptions as |itemOption|}}
|
{{#each this.itemOptions as |itemOption|}}
|
||||||
@@ -47,13 +54,15 @@
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<DD.Footer @hasDivider={{true}}>
|
{{#unless @isLoading}}
|
||||||
<Hds::Button
|
<DD.Footer @hasDivider={{true}}>
|
||||||
@text='Apply'
|
<Hds::Button
|
||||||
@isFullWidth={{true}}
|
@text='Apply'
|
||||||
@size='small'
|
@isFullWidth={{true}}
|
||||||
{{on 'click' (fn this.applyFilter DD.close)}}
|
@size='small'
|
||||||
data-test-dropdown-apply-button
|
{{on 'click' (fn this.applyFilter DD.close)}}
|
||||||
/>
|
data-test-dropdown-apply-button
|
||||||
</DD.Footer>
|
/>
|
||||||
|
</DD.Footer>
|
||||||
|
{{/unless}}
|
||||||
</Hds::Dropdown>
|
</Hds::Dropdown>
|
||||||
@@ -21,7 +21,8 @@ export default class DropdownComponent extends Component {
|
|||||||
*/
|
*/
|
||||||
get itemOptions() {
|
get itemOptions() {
|
||||||
let items = this.args.itemOptions;
|
let items = this.args.itemOptions;
|
||||||
if (this.searchTerm) {
|
|
||||||
|
if (this.searchTerm && !this.args.updateSearchTerm) {
|
||||||
const searchTerm = this.searchTerm.toLowerCase();
|
const searchTerm = this.searchTerm.toLowerCase();
|
||||||
items = this.args.itemOptions.filter((item) => {
|
items = this.args.itemOptions.filter((item) => {
|
||||||
const isNameMatch = item.name?.toLowerCase().includes(searchTerm);
|
const isNameMatch = item.name?.toLowerCase().includes(searchTerm);
|
||||||
@@ -29,7 +30,7 @@ export default class DropdownComponent extends Component {
|
|||||||
return isNameMatch || isIdMatch;
|
return isNameMatch || isIdMatch;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return items.slice(0, 500);
|
return items.slice(0, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =methods
|
// =methods
|
||||||
@@ -39,10 +40,15 @@ export default class DropdownComponent extends Component {
|
|||||||
* @param {object} event
|
* @param {object} event
|
||||||
*/
|
*/
|
||||||
@action
|
@action
|
||||||
@debounce(150)
|
@debounce(250)
|
||||||
filterItems(event) {
|
async filterItems(event) {
|
||||||
const { value } = event.target;
|
const { value } = event.target;
|
||||||
this.searchTerm = value;
|
|
||||||
|
if (this.args.updateSearchTerm) {
|
||||||
|
this.args.updateSearchTerm(value);
|
||||||
|
} else {
|
||||||
|
this.searchTerm = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,16 +17,27 @@ export default class FilterTagsIndexComponent extends Component {
|
|||||||
|
|
||||||
get filters() {
|
get filters() {
|
||||||
const { allFilters, selectedFilters } = this.args.filters;
|
const { allFilters, selectedFilters } = this.args.filters;
|
||||||
return Object.entries(allFilters).flatMap(([key, value]) => {
|
return Object.entries(allFilters).flatMap(([key, values]) => {
|
||||||
assert(`Tags must be an array for key ${key}`, Array.isArray(value));
|
assert(`Tags must be an array for key ${key}`, Array.isArray(values));
|
||||||
const paramsSet = new Set(selectedFilters[key]);
|
const uniqueSelectedFilters = [...new Set(selectedFilters[key])];
|
||||||
const filters = value.filter((item) => paramsSet.has(item.id));
|
if (!uniqueSelectedFilters.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return filters.map((item) => ({
|
const valueMap = new Map(values.map((value) => [value.id, value.name]));
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
return uniqueSelectedFilters.filter(Boolean).map((item) => {
|
||||||
type: key,
|
const filter = {
|
||||||
}));
|
id: item,
|
||||||
|
type: key,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (valueMap.has(item)) {
|
||||||
|
filter.name = valueMap.get(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,8 @@
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
{{#if @model.type}}
|
{{#if @model.type}}
|
||||||
{{component
|
<this.authenticateFormComponent
|
||||||
(concat 'form/authenticate/' @model.type)
|
@onSubmit={{@submit}}
|
||||||
onSubmit=@submit
|
@disabled={{@disabled}}
|
||||||
disabled=@disabled
|
/>
|
||||||
}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { assert } from '@ember/debug';
|
||||||
|
import oidc from './oidc';
|
||||||
|
import ldap from './ldap';
|
||||||
|
import password from './password';
|
||||||
|
|
||||||
|
const modelTypeToComponent = {
|
||||||
|
ldap,
|
||||||
|
password,
|
||||||
|
oidc,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class FormAuthenticateIndex extends Component {
|
||||||
|
get authenticateFormComponent() {
|
||||||
|
const component = modelTypeToComponent[this.args.model.type];
|
||||||
|
assert(
|
||||||
|
`Mapped component must exist for auth method: ${this.args.model.type}`,
|
||||||
|
component,
|
||||||
|
);
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -129,8 +129,9 @@ class RouteResourceFilter extends EmberObject {
|
|||||||
const filterKey = `filter-${name}`;
|
const filterKey = `filter-${name}`;
|
||||||
|
|
||||||
// If the route has no query params specified yet, declare them.
|
// If the route has no query params specified yet, declare them.
|
||||||
if (!Object.keys(routeClass.queryParams).length)
|
if (!Object.keys(routeClass.queryParams).length) {
|
||||||
routeClass.queryParams = {};
|
routeClass.queryParams = {};
|
||||||
|
}
|
||||||
|
|
||||||
// If the route has no resource filters list specified yet, create one.
|
// If the route has no resource filters list specified yet, create one.
|
||||||
if (!routeClass.resourceFilterParams) routeClass.resourceFilterParams = [];
|
if (!routeClass.resourceFilterParams) routeClass.resourceFilterParams = [];
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@use 'rose/app/styles/rose/variables/sizing' as sizing;
|
||||||
|
@use 'sass:meta';
|
||||||
|
|
||||||
.filters-applied {
|
.filters-applied {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -72,3 +75,34 @@
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$margin-directions: (
|
||||||
|
'': 'margin',
|
||||||
|
'-top': 'margin-top',
|
||||||
|
'-right': 'margin-right',
|
||||||
|
'-bottom': 'margin-bottom',
|
||||||
|
'-left': 'margin-left',
|
||||||
|
'-x': (
|
||||||
|
'margin-left',
|
||||||
|
'margin-right',
|
||||||
|
),
|
||||||
|
'-y': (
|
||||||
|
'margin-top',
|
||||||
|
'margin-bottom',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate margin utility classes with sizes and directions
|
||||||
|
@each $size-name, $size-coefficient in sizing.$size-coefficients {
|
||||||
|
@each $direction-suffix, $properties in $margin-directions {
|
||||||
|
.margin#{$direction-suffix}-#{$size-name} {
|
||||||
|
@if meta.type-of($properties) == 'list' {
|
||||||
|
@each $property in $properties {
|
||||||
|
#{$property}: #{sizing.rems($size-name)};
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
#{$properties}: #{sizing.rems($size-name)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default } from 'core/components/form/authenticate/index';
|
||||||
@@ -127,7 +127,7 @@ module('Integration | Component | dropdown/index', function (hooks) {
|
|||||||
assert.true(result);
|
assert.true(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it only renders a max of 500 items but allows searching on all items', async function (assert) {
|
test('it only renders a max of 250 items but allows searching on all items', async function (assert) {
|
||||||
this.set('checkedItems', []);
|
this.set('checkedItems', []);
|
||||||
this.set(
|
this.set(
|
||||||
'itemOptions',
|
'itemOptions',
|
||||||
@@ -145,7 +145,7 @@ module('Integration | Component | dropdown/index', function (hooks) {
|
|||||||
|
|
||||||
await click(TOGGLE_DROPDOWN_SELECTOR);
|
await click(TOGGLE_DROPDOWN_SELECTOR);
|
||||||
|
|
||||||
assert.dom(ITEM_SELECTOR).exists({ count: 500 });
|
assert.dom(ITEM_SELECTOR).exists({ count: 250 });
|
||||||
|
|
||||||
// Random number between 500 and 599 to search
|
// Random number between 500 and 599 to search
|
||||||
await fillIn(SEARCH_INPUT_SELECTOR, '591');
|
await fillIn(SEARCH_INPUT_SELECTOR, '591');
|
||||||
|
|||||||
@@ -22,4 +22,14 @@ module('Integration | Component | filter-tags/index', function (hooks) {
|
|||||||
|
|
||||||
assert.dom('.hds-tag__text').hasText('Project 1');
|
assert.dom('.hds-tag__text').hasText('Project 1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it renders id if allFilter does not have corresponding value', async function (assert) {
|
||||||
|
this.set('filter', {
|
||||||
|
allFilters: { scopes: [{ id: '2', name: 'Project 2' }] },
|
||||||
|
selectedFilters: { scopes: ['1'] },
|
||||||
|
});
|
||||||
|
await render(hbs`<FilterTags @filters={{this.filter}} />`);
|
||||||
|
|
||||||
|
assert.dom('.hds-tag__text').hasText('1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -68,3 +68,4 @@ show-errors: Show Errors
|
|||||||
hide-errors: Hide Errors
|
hide-errors: Hide Errors
|
||||||
edit-worker-filter: Edit Worker Filter
|
edit-worker-filter: Edit Worker Filter
|
||||||
add-worker-filter: Add Worker Filter
|
add-worker-filter: Add Worker Filter
|
||||||
|
open: Open
|
||||||
@@ -49,6 +49,7 @@ titles:
|
|||||||
project-actions: Project Actions
|
project-actions: Project Actions
|
||||||
user-menu: User Menu
|
user-menu: User Menu
|
||||||
back-link: 'Back to {scope}'
|
back-link: 'Back to {scope}'
|
||||||
|
authenticated: Signed in as
|
||||||
descriptions:
|
descriptions:
|
||||||
empty-set: There are no items to display yet. You may be able to add items or try back later.
|
empty-set: There are no items to display yet. You may be able to add items or try back later.
|
||||||
cluster-url-initialization: To get started, please enter your cluster URL
|
cluster-url-initialization: To get started, please enter your cluster URL
|
||||||
@@ -141,6 +142,21 @@ settings:
|
|||||||
alerts:
|
alerts:
|
||||||
cache-daemon: There may be a problem with the cache daemon
|
cache-daemon: There may be a problem with the cache daemon
|
||||||
client-agent: There may be a problem with the client agent
|
client-agent: There may be a problem with the client agent
|
||||||
|
preferred-clients:
|
||||||
|
title: Preferred Clients
|
||||||
|
description: Select the applications for Boundary to use when opening these target types.
|
||||||
|
table:
|
||||||
|
header:
|
||||||
|
protocol: Protocol
|
||||||
|
client: Client
|
||||||
|
data:
|
||||||
|
protocols:
|
||||||
|
windows-rdp: Windows RDP
|
||||||
|
clients:
|
||||||
|
none: None
|
||||||
|
mstsc: Remote Desktop Connection (mstsc)
|
||||||
|
windows-app: Windows App
|
||||||
|
none-detected: 'None detected. We recommend <a href={rdpClientLink} target="_blank" rel="noopener noreferrer">{rdpClientName}</a>.'
|
||||||
worker-filter-generator:
|
worker-filter-generator:
|
||||||
title: Filter generator
|
title: Filter generator
|
||||||
description: Choose what you want to format into a filter.
|
description: Choose what you want to format into a filter.
|
||||||
|
|||||||
@@ -913,11 +913,12 @@ credential-library:
|
|||||||
ssh_private_key: SSH Private Key
|
ssh_private_key: SSH Private Key
|
||||||
username_password: Username & Password
|
username_password: Username & Password
|
||||||
username_password_domain: Username, Password & Domain
|
username_password_domain: Username, Password & Domain
|
||||||
username_attribute: Username
|
password: Password
|
||||||
password_attribute: Password
|
username_attribute: username_attribute
|
||||||
private_key_passphrase_attribute: Private Key Passphrase
|
password_attribute: password_attribute
|
||||||
private_key_attribute: Private Key
|
private_key_passphrase_attribute: private_key_passphrase_attribute
|
||||||
domain_attribute: Domain
|
private_key_attribute: private_key_attribute
|
||||||
|
domain_attribute: domain_attribute
|
||||||
actions:
|
actions:
|
||||||
create: New Credential Library
|
create: New Credential Library
|
||||||
delete: Delete Credential Library
|
delete: Delete Credential Library
|
||||||
@@ -989,12 +990,14 @@ credential:
|
|||||||
ssh_private_key: Username & Key Pair
|
ssh_private_key: Username & Key Pair
|
||||||
username_password_domain: Username, Password & Domain
|
username_password_domain: Username, Password & Domain
|
||||||
json: JSON
|
json: JSON
|
||||||
|
password: Password
|
||||||
unknown: Unknown
|
unknown: Unknown
|
||||||
help:
|
help:
|
||||||
username_password: Connect using the provided username and password.
|
username_password: Connect using the provided username and password.
|
||||||
ssh_private_key: Connect using a username, public key, and private key.
|
ssh_private_key: Connect using a username, public key, and private key.
|
||||||
json: Connect using a JSON blob containing the credentials.
|
json: Connect using a JSON blob containing the credentials.
|
||||||
username_password_domain: Includes a domain field for Active Directory
|
username_password_domain: Includes a domain field for Active Directory.
|
||||||
|
password: Connect using only a provided password.
|
||||||
form:
|
form:
|
||||||
username:
|
username:
|
||||||
label: Username
|
label: Username
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ module.exports = {
|
|||||||
browser: true,
|
browser: true,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
curly: ['error', 'multi-line', 'consistent'],
|
||||||
'ember/no-jquery': 'error',
|
'ember/no-jquery': 'error',
|
||||||
'ember/no-new-mixins': 0,
|
'ember/no-new-mixins': 0,
|
||||||
'ember/no-get': 'off',
|
'ember/no-get': 'off',
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ form {
|
|||||||
.hds-table__th-sort button {
|
.hds-table__th-sort button {
|
||||||
color: var(--token-color-foreground-strong);
|
color: var(--token-color-foreground-strong);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: This is a temporary hack to fix an issue causing weird whitespace
|
// TODO: This is a temporary hack to fix an issue causing weird whitespace
|
||||||
// and scrollbars from an accessible class in HDS. We have to use !important
|
// and scrollbars from an accessible class in HDS. We have to use !important
|
||||||
// as they already forced it with !important and we can't override it otherwise
|
// as they already forced it with !important and we can't override it otherwise
|
||||||
.sr-only {
|
.sr-only {
|
||||||
position: unset !important;
|
position: unset !important;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hds-dropdown {
|
.hds-dropdown {
|
||||||
|
|||||||
@@ -121,14 +121,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin dark-mode-ember-dropdown-override {
|
||||||
|
.ember-basic-dropdown-content {
|
||||||
|
background-color: var(--token-color-palette-neutral-0);
|
||||||
|
|
||||||
|
/* stylelint-disable-next-line selector-class-pattern */
|
||||||
|
&.ember-basic-dropdown-content--in-place {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.ember-application:not(.rose-theme-light) {
|
.ember-application:not(.rose-theme-light) {
|
||||||
@include dark-mode;
|
@include dark-mode;
|
||||||
@include dark-mode-modal-override;
|
@include dark-mode-modal-override;
|
||||||
|
@include dark-mode-ember-dropdown-override;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ember-application.rose-theme-dark {
|
.ember-application.rose-theme-dark {
|
||||||
@include dark-mode;
|
@include dark-mode;
|
||||||
@include dark-mode-modal-override;
|
@include dark-mode-modal-override;
|
||||||
|
@include dark-mode-ember-dropdown-override;
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ html {
|
|||||||
body {
|
body {
|
||||||
// Used by Rose::Tabs and '/' in breadcrumbs
|
// Used by Rose::Tabs and '/' in breadcrumbs
|
||||||
// and link list panel
|
// and link list panel
|
||||||
font-family: typography.$family-primary;
|
font-family: var(--token-typography-font-stack-text);
|
||||||
|
|
||||||
// this is used across the app
|
// this is used across the app
|
||||||
line-height: typography.line-height(m);
|
line-height: typography.line-height(m);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: ui-monospace, Menlo, monospace;
|
font-family: var(--token-typography-font-stack-code);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rose-autosize {
|
.rose-autosize {
|
||||||
|
|||||||
@@ -28,9 +28,10 @@
|
|||||||
|
|
||||||
.rose-form-checkbox-label {
|
.rose-form-checkbox-label {
|
||||||
// label styles
|
// label styles
|
||||||
@include type.type(s, semibold);
|
@include type.type(s);
|
||||||
|
|
||||||
color: currentcolor;
|
color: currentcolor;
|
||||||
|
font-weight: var(--token-typography-font-weight-semibold);
|
||||||
|
|
||||||
&,
|
&,
|
||||||
.rose-form-checkbox-label-description {
|
.rose-form-checkbox-label-description {
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
content: ' ';
|
content: ' ';
|
||||||
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
|
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
|
||||||
border-radius: sizing.rems(xxxs);
|
border-radius: var(--token-form-checkbox-border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
margin-bottom: sizing.rems(m);
|
margin-bottom: sizing.rems(m);
|
||||||
|
|
||||||
.rose-form-fieldset-title {
|
.rose-form-fieldset-title {
|
||||||
@include type.type(s, semibold);
|
@include type.type(s);
|
||||||
|
|
||||||
|
font-weight: var(--token-typography-font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rose-form-fieldset-body {
|
.rose-form-fieldset-body {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
margin-bottom: sizing.rems(xs);
|
margin-bottom: sizing.rems(xs);
|
||||||
padding: sizing.rems(xs) sizing.rems(xxl) sizing.rems(xs) sizing.rems(xs);
|
padding: sizing.rems(xs) sizing.rems(xxl) sizing.rems(xs) sizing.rems(xs);
|
||||||
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
|
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
|
||||||
border-radius: sizing.rems(xxxs);
|
border-radius: var(--token-border-radius-x-small);
|
||||||
box-shadow: 0 sizing.rems(xxs) sizing.rems(xxs)
|
box-shadow: 0 sizing.rems(xxs) sizing.rems(xxs)
|
||||||
rgba(var(--stark-components), var(--opacity-2));
|
rgba(var(--stark-components), var(--opacity-2));
|
||||||
background: var(--ui-gray-subtler-5)
|
background: var(--ui-gray-subtler-5)
|
||||||
|
|||||||
@@ -20,8 +20,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rose-header-brand-text {
|
.rose-header-brand-text {
|
||||||
@include type.type(m, normal);
|
@include type.type(m);
|
||||||
|
|
||||||
|
font-weight: var(--token-typography-font-weight-regular);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-left: sizing.rems(s);
|
margin-left: sizing.rems(s);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
// First cell (key) is approximately 25%
|
// First cell (key) is approximately 25%
|
||||||
&:nth-child(1) {
|
&:nth-child(1) {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
font-weight: typography.font-weight(semibold);
|
font-weight: var(--token-typography-font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second cell (value) is approximately 75%
|
// Second cell (value) is approximately 75%
|
||||||
|
|||||||
@@ -22,10 +22,11 @@
|
|||||||
|
|
||||||
// TODO revisit the title
|
// TODO revisit the title
|
||||||
.rose-nav-title {
|
.rose-nav-title {
|
||||||
@include type.type(xs, semibold);
|
@include type.type(xs);
|
||||||
|
|
||||||
display: none;
|
display: none;
|
||||||
color: #{sass.color(ui-gray, 400)};
|
color: #{sass.color(ui-gray, 400)};
|
||||||
|
font-weight: var(--token-typography-font-weight-semibold);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
$line-height: math.div(20, 12);
|
$line-height: math.div(20, 12);
|
||||||
|
|||||||
@@ -17,8 +17,9 @@
|
|||||||
--background: transparent;
|
--background: transparent;
|
||||||
--box-shadow-color: transparent;
|
--box-shadow-color: transparent;
|
||||||
|
|
||||||
@include type.type(s, semibold);
|
@include type.type(s);
|
||||||
|
|
||||||
|
font-weight: var(--token-typography-font-weight-semibold);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
|
|||||||
@@ -9,10 +9,7 @@
|
|||||||
* Mixin for specifying a font-size/line-height size on an element, with
|
* Mixin for specifying a font-size/line-height size on an element, with
|
||||||
* an option for specifying weight.
|
* an option for specifying weight.
|
||||||
*/
|
*/
|
||||||
@mixin type($size, $weight: &) {
|
@mixin type($size) {
|
||||||
font-size: typography.font-size($size);
|
font-size: typography.font-size($size);
|
||||||
line-height: typography.line-height($size);
|
line-height: typography.line-height($size);
|
||||||
@if $weight {
|
|
||||||
font-weight: typography.font-weight($weight);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -11,29 +11,6 @@
|
|||||||
* SASS variables defining typography.
|
* SASS variables defining typography.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$family-primary:
|
|
||||||
system-ui,
|
|
||||||
-apple-system,
|
|
||||||
blinkmacsystemfont,
|
|
||||||
'Segoe UI',
|
|
||||||
'Roboto',
|
|
||||||
'Oxygen',
|
|
||||||
'Ubuntu',
|
|
||||||
'Cantarell',
|
|
||||||
'Fira Sans',
|
|
||||||
'Droid Sans',
|
|
||||||
'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
$family-monospace:
|
|
||||||
'SFMono-Regular', consolas, 'Liberation Mono', menlo, courier, monospace;
|
|
||||||
|
|
||||||
// Available font weights by name
|
|
||||||
$font-weights: (
|
|
||||||
normal: 400,
|
|
||||||
semibold: 600,
|
|
||||||
bold: 700,
|
|
||||||
);
|
|
||||||
|
|
||||||
// unitless value to start, but think pixels or points
|
// unitless value to start, but think pixels or points
|
||||||
$base-font-size: 16;
|
$base-font-size: 16;
|
||||||
|
|
||||||
@@ -84,10 +61,3 @@ $line-heights: (
|
|||||||
@function line-height($name) {
|
@function line-height($name) {
|
||||||
@return map.get($line-heights, $name);
|
@return map.get($line-heights, $name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a named font weight.
|
|
||||||
*/
|
|
||||||
@function font-weight($name) {
|
|
||||||
@return map.get($font-weights, $name);
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.26.10",
|
"@babel/core": "^7.26.10",
|
||||||
"@hashicorp/design-system-components": "^4.20.2",
|
"@hashicorp/design-system-components": "^4.24.1",
|
||||||
"@hashicorp/design-system-tokens": "^2.3.0",
|
"@hashicorp/design-system-tokens": "^2.3.0",
|
||||||
"@hashicorp/flight-icons": "^3.10.0",
|
"@hashicorp/flight-icons": "^3.10.0",
|
||||||
"@nullvoxpopuli/ember-composable-helpers": "^5.2.10",
|
"@nullvoxpopuli/ember-composable-helpers": "^5.2.10",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
width: 9rem;
|
width: 9rem;
|
||||||
|
|
||||||
// border: 1px solid var(--ui-gray-subtler-4);
|
// border: 1px solid var(--ui-gray-subtler-4);
|
||||||
border-radius: 4px;
|
border-radius: var(--token-border-radius-small);
|
||||||
margin: 0 0.5rem 0.5rem 0 !important;
|
margin: 0 0.5rem 0.5rem 0 !important;
|
||||||
box-shadow: 0 3px 6px rgba(var(--stark-components), var(--opacity-3));
|
box-shadow: 0 3px 6px rgba(var(--stark-components), var(--opacity-3));
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
.swatch-chip {
|
.swatch-chip {
|
||||||
height: 5rem;
|
height: 5rem;
|
||||||
border: 0.5rem solid var(--subtle);
|
border: 0.5rem solid var(--subtle);
|
||||||
border-radius: 4px;
|
border-radius: var(--token-border-radius-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
.swatch-detail {
|
.swatch-detail {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ module.exports = {
|
|||||||
es2021: true,
|
es2021: true,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
curly: ['error', 'multi-line', 'consistent'],
|
||||||
'no-empty-pattern': ['error', { allowObjectPatternsAsParameters: true }],
|
'no-empty-pattern': ['error', { allowObjectPatternsAsParameters: true }],
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
|
|||||||
@@ -147,11 +147,11 @@ export class CredentialStoresPage extends BaseResourcePage {
|
|||||||
await this.page.getByRole('link', { name: 'New Credential' }).click();
|
await this.page.getByRole('link', { name: 'New Credential' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.page.getByLabel('Name (Optional)').fill(credentialName);
|
await this.page.getByLabel('Name', { exact: true }).fill(credentialName);
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
await this.page.getByLabel('Description').fill('This is an automated test');
|
||||||
|
await this.page.getByRole('combobox', { name: 'Type' }).click();
|
||||||
await this.page
|
await this.page
|
||||||
.getByRole('group', { name: 'Type' })
|
.getByRole('option', { name: 'Username & Key Pair' })
|
||||||
.getByLabel('Username & Key Pair')
|
|
||||||
.click();
|
.click();
|
||||||
await this.page
|
await this.page
|
||||||
.getByLabel('Username Required', { exact: true })
|
.getByLabel('Username Required', { exact: true })
|
||||||
@@ -207,11 +207,11 @@ export class CredentialStoresPage extends BaseResourcePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const credentialName = 'Credential ' + nanoid();
|
const credentialName = 'Credential ' + nanoid();
|
||||||
await this.page.getByLabel('Name (Optional)').fill(credentialName);
|
await this.page.getByLabel('Name', { exact: true }).fill(credentialName);
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
await this.page.getByLabel('Description').fill('This is an automated test');
|
||||||
|
await this.page.getByRole('combobox', { name: 'Type' }).click();
|
||||||
await this.page
|
await this.page
|
||||||
.getByRole('group', { name: 'Type' })
|
.getByRole('option', { name: 'Username & Password' })
|
||||||
.getByLabel('Username & Password')
|
|
||||||
.click();
|
.click();
|
||||||
await this.page
|
await this.page
|
||||||
.getByLabel('Username Required', { exact: true })
|
.getByLabel('Username Required', { exact: true })
|
||||||
@@ -271,11 +271,11 @@ export class CredentialStoresPage extends BaseResourcePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const credentialName = 'Credential ' + nanoid();
|
const credentialName = 'Credential ' + nanoid();
|
||||||
await this.page.getByLabel('Name (Optional)').fill(credentialName);
|
await this.page.getByLabel('Name', { exact: true }).fill(credentialName);
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
await this.page.getByLabel('Description').fill('This is an automated test');
|
||||||
|
await this.page.getByRole('combobox', { name: 'Type' }).click();
|
||||||
await this.page
|
await this.page
|
||||||
.getByRole('group', { name: 'Type' })
|
.getByRole('option', { name: 'Username, Password & Domain' })
|
||||||
.getByLabel('Username, Password & Domain')
|
|
||||||
.click();
|
.click();
|
||||||
await this.page
|
await this.page
|
||||||
.getByLabel('Username Required', { exact: true })
|
.getByLabel('Username Required', { exact: true })
|
||||||
@@ -308,11 +308,9 @@ export class CredentialStoresPage extends BaseResourcePage {
|
|||||||
await this.page.getByRole('link', { name: 'Credential Libraries' }).click();
|
await this.page.getByRole('link', { name: 'Credential Libraries' }).click();
|
||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
||||||
await this.page
|
await this.page
|
||||||
.getByLabel('Name (Optional)', { exact: true })
|
.getByLabel('Name', { exact: true })
|
||||||
.fill(credentialLibraryName);
|
.fill(credentialLibraryName);
|
||||||
await this.page
|
await this.page.getByLabel('Description').fill('This is an automated test');
|
||||||
.getByLabel('Description (Optional)')
|
|
||||||
.fill('This is an automated test');
|
|
||||||
await this.page
|
await this.page
|
||||||
.getByRole('group', { name: 'Type' })
|
.getByRole('group', { name: 'Type' })
|
||||||
.getByLabel('Generic Secrets')
|
.getByLabel('Generic Secrets')
|
||||||
@@ -345,11 +343,9 @@ export class CredentialStoresPage extends BaseResourcePage {
|
|||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
||||||
|
|
||||||
await this.page
|
await this.page
|
||||||
.getByLabel('Name (Optional)', { exact: true })
|
.getByLabel('Name', { exact: true })
|
||||||
.fill(credentialLibraryName);
|
.fill(credentialLibraryName);
|
||||||
await this.page
|
await this.page.getByLabel('Description').fill('This is an automated test');
|
||||||
.getByLabel('Description (Optional)')
|
|
||||||
.fill('This is an automated test');
|
|
||||||
await this.page
|
await this.page
|
||||||
.getByRole('group', { name: 'Type' })
|
.getByRole('group', { name: 'Type' })
|
||||||
.getByLabel('SSH Certificates')
|
.getByLabel('SSH Certificates')
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export class LoginPage extends BaseResourcePage {
|
|||||||
await this.page.getByRole('button', { name: 'Sign In' }).click();
|
await this.page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
await expect(
|
await expect(
|
||||||
this.page.getByRole('navigation', {
|
this.page.getByRole('navigation', {
|
||||||
// TODO: Uncomment line below once both UIs have been refactored to use HDS side nav.
|
name: 'Application local navigation',
|
||||||
// name: 'Application local navigation',
|
|
||||||
}),
|
}),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(this.page.getByText(loginName)).toBeEnabled();
|
await expect(this.page.getByText(loginName)).toBeEnabled();
|
||||||
|
|||||||
@@ -60,16 +60,15 @@ export class OrgsPage extends BaseResourcePage {
|
|||||||
async chooseScopeFromDropdown(fromScope, toScope) {
|
async chooseScopeFromDropdown(fromScope, toScope) {
|
||||||
await this.page
|
await this.page
|
||||||
.getByRole('button', { name: fromScope, exact: true })
|
.getByRole('button', { name: fromScope, exact: true })
|
||||||
.click()
|
.click();
|
||||||
await this.page
|
await this.page.getByRole('option', { name: toScope, exact: true }).click();
|
||||||
.getByRole('option', { name: toScope, exact: true })
|
|
||||||
.click()
|
|
||||||
await expect(
|
await expect(
|
||||||
this.page.getByRole('button', { name: toScope, exact: true })
|
this.page.getByRole('button', { name: toScope, exact: true }),
|
||||||
).toBeVisible()
|
).toBeVisible();
|
||||||
if (toScope != 'Global')
|
if (toScope !== 'Global') {
|
||||||
await expect(
|
await expect(
|
||||||
this.page.getByRole('link', { name: toScope, exact: true })
|
this.page.getByRole('link', { name: toScope, exact: true }),
|
||||||
).toBeVisible()
|
).toBeVisible();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,73 +9,15 @@ import { nanoid } from 'nanoid';
|
|||||||
import { BaseResourcePage } from './base-resource.js';
|
import { BaseResourcePage } from './base-resource.js';
|
||||||
|
|
||||||
export class TargetsPage extends BaseResourcePage {
|
export class TargetsPage extends BaseResourcePage {
|
||||||
/**
|
|
||||||
* Creates a new target. Assumes you have selected the desired project.
|
|
||||||
* @param {string} port Port of the target
|
|
||||||
* @returns Name of the target
|
|
||||||
*/
|
|
||||||
async createTarget(port) {
|
|
||||||
const targetName = 'Target ' + nanoid();
|
|
||||||
await this.page
|
|
||||||
.getByRole('navigation', { name: 'Application local navigation' })
|
|
||||||
.getByRole('link', { name: 'Targets' })
|
|
||||||
.click();
|
|
||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
|
||||||
await this.page.getByLabel('Name').fill(targetName);
|
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
|
||||||
await this.page.getByLabel('Default Port').fill(port);
|
|
||||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
|
||||||
await this.dismissSuccessAlert();
|
|
||||||
await expect(
|
|
||||||
this.page
|
|
||||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
|
||||||
.getByText(targetName),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
return targetName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new target with address. Assumes you have selected the desired project.
|
|
||||||
* @param {string} address Address of the target
|
|
||||||
* @param {string} port Port of the target
|
|
||||||
* @returns Name of the target
|
|
||||||
*/
|
|
||||||
async createTargetWithAddress(address, port) {
|
|
||||||
const targetName = 'Target ' + nanoid();
|
|
||||||
await this.page
|
|
||||||
.getByRole('navigation', { name: 'Application local navigation' })
|
|
||||||
.getByRole('link', { name: 'Targets' })
|
|
||||||
.click();
|
|
||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
|
||||||
await this.page.getByLabel('Name').fill(targetName);
|
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
|
||||||
await this.page.getByLabel('Target Address').fill(address);
|
|
||||||
await this.page.getByLabel('Default Port').fill(port);
|
|
||||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
|
||||||
await this.dismissSuccessAlert();
|
|
||||||
await expect(
|
|
||||||
this.page
|
|
||||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
|
||||||
.getByText(targetName),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
return targetName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new target with address and alias. Assumes you have selected the desired project.
|
* Creates a new target with address and alias. Assumes you have selected the desired project.
|
||||||
|
* @param {string} targetType Type of target (ssh, rdp, tcp)
|
||||||
* @param {string} address Address of the target
|
* @param {string} address Address of the target
|
||||||
* @param {string} port Port of the target
|
* @param {string} port Port of the target
|
||||||
* @param {string} alias alias used for the target
|
* @param {string} alias alias used for the target
|
||||||
* @returns Name of the target
|
* @returns Name of the target
|
||||||
*/
|
*/
|
||||||
async createTargetWithAddressAndAlias(
|
async createTarget({ targetType, port, address, alias }) {
|
||||||
address,
|
|
||||||
port,
|
|
||||||
alias,
|
|
||||||
targetType = 'tcp',
|
|
||||||
) {
|
|
||||||
const targetName = 'Target ' + nanoid();
|
const targetName = 'Target ' + nanoid();
|
||||||
let targetTypeLabel;
|
let targetTypeLabel;
|
||||||
|
|
||||||
@@ -92,9 +34,6 @@ export class TargetsPage extends BaseResourcePage {
|
|||||||
targetTypeLabel = 'Generic TCP';
|
targetTypeLabel = 'Generic TCP';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
|
||||||
throw new Error(`Unexpected target type passed ${targetType}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.page
|
await this.page
|
||||||
@@ -104,258 +43,26 @@ export class TargetsPage extends BaseResourcePage {
|
|||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
||||||
await this.page.getByLabel('Name').fill(targetName);
|
await this.page.getByLabel('Name').fill(targetName);
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
await this.page.getByLabel('Description').fill('This is an automated test');
|
||||||
await this.page.getByLabel(targetTypeLabel).click();
|
if (targetType) {
|
||||||
await this.page.getByLabel('Target Address').fill(address);
|
if (targetTypeLabel) {
|
||||||
await this.page.getByLabel('Default Port').fill(port);
|
await this.page
|
||||||
await this.page
|
.getByRole('group', { name: 'Type' })
|
||||||
.getByRole('group', { name: 'Aliases' })
|
.getByLabel(targetTypeLabel)
|
||||||
.getByLabel('value')
|
.click();
|
||||||
.last()
|
} else {
|
||||||
.fill(alias);
|
throw new Error(`Unsupported target type: ${targetType}`);
|
||||||
await this.page.getByRole('button', { name: 'Add' }).click();
|
}
|
||||||
|
}
|
||||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
if (address) await this.page.getByLabel('Target Address').fill(address);
|
||||||
await this.dismissSuccessAlert();
|
if (port) await this.page.getByLabel('Default Port').fill(port);
|
||||||
await expect(
|
if (alias) {
|
||||||
this.page
|
await this.page
|
||||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
.getByRole('group', { name: 'Aliases' })
|
||||||
.getByText(targetName),
|
.getByLabel('value')
|
||||||
).toBeVisible();
|
.last()
|
||||||
|
.fill(alias);
|
||||||
return targetName;
|
await this.page.getByRole('button', { name: 'Add' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new TCP target with address in boundary-enterprise
|
|
||||||
* Assumes you have selected the desired project.
|
|
||||||
* @param {string} address Address of the target
|
|
||||||
* @param {string} port Port of the target
|
|
||||||
* @returns Name of the target
|
|
||||||
*/
|
|
||||||
async createTcpTargetWithAddressEnt(address, port) {
|
|
||||||
const targetName = 'Target ' + nanoid();
|
|
||||||
await this.page
|
|
||||||
.getByRole('navigation', { name: 'Application local navigation' })
|
|
||||||
.getByRole('link', { name: 'Targets' })
|
|
||||||
.click();
|
|
||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
|
||||||
await this.page.getByLabel('Name').fill(targetName);
|
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
|
||||||
await this.page
|
|
||||||
.getByRole('group', { name: 'Type' })
|
|
||||||
.getByLabel('TCP')
|
|
||||||
.click();
|
|
||||||
await this.page.getByLabel('Target Address').fill(address);
|
|
||||||
await this.page.getByLabel('Default Port').fill(port);
|
|
||||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
|
||||||
await this.dismissSuccessAlert();
|
|
||||||
await expect(
|
|
||||||
this.page
|
|
||||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
|
||||||
.getByText(targetName),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
return targetName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new SSH target. Assumes you have selected the desired project.
|
|
||||||
* @param {string} port Port of the target
|
|
||||||
* @returns Name of the target
|
|
||||||
*/
|
|
||||||
async createSshTargetEnt(port) {
|
|
||||||
const targetName = 'Target ' + nanoid();
|
|
||||||
await this.page
|
|
||||||
.getByRole('navigation', { name: 'Application local navigation' })
|
|
||||||
.getByRole('link', { name: 'Targets' })
|
|
||||||
.click();
|
|
||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
|
||||||
await this.page.getByLabel('Name').fill(targetName);
|
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
|
||||||
await this.page
|
|
||||||
.getByRole('group', { name: 'Type' })
|
|
||||||
.getByLabel('SSH')
|
|
||||||
.click();
|
|
||||||
await this.page.getByLabel('Default Port').fill(port);
|
|
||||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
|
||||||
await this.dismissSuccessAlert();
|
|
||||||
await expect(
|
|
||||||
this.page
|
|
||||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
|
||||||
.getByText(targetName),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
return targetName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new SSH target with address in boundary-enterprise
|
|
||||||
* Assumes you have selected the desired project.
|
|
||||||
* @param {string} address Address of the target
|
|
||||||
* @param {string} port Port of the target
|
|
||||||
* @returns Name of the target
|
|
||||||
*/
|
|
||||||
async createSshTargetWithAddressEnt(address, port) {
|
|
||||||
const targetName = 'Target ' + nanoid();
|
|
||||||
await this.page
|
|
||||||
.getByRole('navigation', { name: 'Application local navigation' })
|
|
||||||
.getByRole('link', { name: 'Targets' })
|
|
||||||
.click();
|
|
||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
|
||||||
await this.page.getByLabel('Name').fill(targetName);
|
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
|
||||||
await this.page
|
|
||||||
.getByRole('group', { name: 'Type' })
|
|
||||||
.getByLabel('SSH')
|
|
||||||
.click();
|
|
||||||
await this.page.getByLabel('Target Address').fill(address);
|
|
||||||
await this.page.getByLabel('Default Port').fill(port);
|
|
||||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
|
||||||
await this.dismissSuccessAlert();
|
|
||||||
await expect(
|
|
||||||
this.page
|
|
||||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
|
||||||
.getByText(targetName),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
return targetName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new SSH target with address and alias.
|
|
||||||
* Assumes you have selected the desired project.
|
|
||||||
* @param {string} address Address of the target
|
|
||||||
* @param {string} port Port of the target
|
|
||||||
* @param {string} alias alias used for the target
|
|
||||||
* @returns Name of the target
|
|
||||||
*/
|
|
||||||
async createSshTargetWithAddressAndAlias(address, port, alias) {
|
|
||||||
const targetName = 'Target ' + nanoid();
|
|
||||||
await this.page
|
|
||||||
.getByRole('navigation', { name: 'Application local navigation' })
|
|
||||||
.getByRole('link', { name: 'Targets' })
|
|
||||||
.click();
|
|
||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
|
||||||
await this.page.getByLabel('Name').fill(targetName);
|
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
|
||||||
await this.page
|
|
||||||
.getByRole('group', { name: 'Type' })
|
|
||||||
.getByLabel('SSH')
|
|
||||||
.click();
|
|
||||||
await this.page.getByLabel('Target Address').fill(address);
|
|
||||||
await this.page.getByLabel('Default Port').fill(port);
|
|
||||||
await this.page
|
|
||||||
.getByRole('group', { name: 'Aliases' })
|
|
||||||
.getByLabel('value')
|
|
||||||
.last()
|
|
||||||
.fill(alias);
|
|
||||||
await this.page.getByRole('button', { name: 'Add' }).click();
|
|
||||||
|
|
||||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
|
||||||
await this.dismissSuccessAlert();
|
|
||||||
await expect(
|
|
||||||
this.page
|
|
||||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
|
||||||
.getByText(targetName),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
return targetName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new RDP target.
|
|
||||||
* Assumes you have selected the desired project.
|
|
||||||
* @param {string} port Port of the target
|
|
||||||
* @returns Name of the target
|
|
||||||
*/
|
|
||||||
async createRdpTargetEnt(port) {
|
|
||||||
const targetName = 'Target ' + nanoid();
|
|
||||||
await this.page
|
|
||||||
.getByRole('navigation', { name: 'Application local navigation' })
|
|
||||||
.getByRole('link', { name: 'Targets' })
|
|
||||||
.click();
|
|
||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
|
||||||
await this.page.getByLabel('Name').fill(targetName);
|
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
|
||||||
await this.page
|
|
||||||
.getByRole('group', { name: 'Type' })
|
|
||||||
.getByLabel('RDP')
|
|
||||||
.click();
|
|
||||||
await this.page.getByLabel('Default Port').fill(port);
|
|
||||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
|
||||||
await this.dismissSuccessAlert();
|
|
||||||
await expect(
|
|
||||||
this.page
|
|
||||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
|
||||||
.getByText(targetName),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
return targetName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new RDP target with address in boundary-enterprise
|
|
||||||
* Assumes you have selected the desired project.
|
|
||||||
* @param {string} address Address of the target
|
|
||||||
* @param {string} port Port of the target
|
|
||||||
* @returns Name of the target
|
|
||||||
*/
|
|
||||||
async createRDPTargetWithAddressEnt(address, port) {
|
|
||||||
const targetName = 'Target ' + nanoid();
|
|
||||||
await this.page
|
|
||||||
.getByRole('navigation', { name: 'Application local navigation' })
|
|
||||||
.getByRole('link', { name: 'Targets' })
|
|
||||||
.click();
|
|
||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
|
||||||
await this.page.getByLabel('Name').fill(targetName);
|
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
|
||||||
await this.page
|
|
||||||
.getByRole('group', { name: 'Type' })
|
|
||||||
.getByLabel('RDP')
|
|
||||||
.click();
|
|
||||||
await this.page.getByLabel('Target Address').fill(address);
|
|
||||||
await this.page.getByLabel('Default Port').fill(port);
|
|
||||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
|
||||||
await this.dismissSuccessAlert();
|
|
||||||
await expect(
|
|
||||||
this.page
|
|
||||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
|
||||||
.getByText(targetName),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
return targetName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new RDP target with address and alias.
|
|
||||||
* Assumes you have selected the desired project.
|
|
||||||
* @param {string} address Address of the target
|
|
||||||
* @param {string} port Port of the target
|
|
||||||
* @param {string} alias alias used for the target
|
|
||||||
* @returns Name of the target
|
|
||||||
*/
|
|
||||||
|
|
||||||
async createRdpTargetWithAddressAndAlias(address, port, alias) {
|
|
||||||
const targetName = 'Target ' + nanoid();
|
|
||||||
await this.page
|
|
||||||
.getByRole('navigation', { name: 'Application local navigation' })
|
|
||||||
.getByRole('link', { name: 'Targets' })
|
|
||||||
.click();
|
|
||||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
|
||||||
await this.page.getByLabel('Name').fill(targetName);
|
|
||||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
|
||||||
await this.page
|
|
||||||
.getByRole('group', { name: 'Type' })
|
|
||||||
.getByLabel('RDP')
|
|
||||||
.click();
|
|
||||||
await this.page.getByLabel('Target Address').fill(address);
|
|
||||||
await this.page.getByLabel('Default Port').fill(port);
|
|
||||||
await this.page
|
|
||||||
.getByRole('group', { name: 'Aliases' })
|
|
||||||
.getByLabel('value')
|
|
||||||
.last()
|
|
||||||
.fill(alias);
|
|
||||||
await this.page.getByRole('button', { name: 'Add' }).click();
|
|
||||||
|
|
||||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
await this.page.getByRole('button', { name: 'Save' }).click();
|
||||||
await this.dismissSuccessAlert();
|
await this.dismissSuccessAlert();
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export class WorkersPage extends BaseResourcePage {
|
|||||||
await this.page
|
await this.page
|
||||||
.getByRole('row')
|
.getByRole('row')
|
||||||
.filter({ has: this.page.getByRole('cell', { name: origKey }) })
|
.filter({ has: this.page.getByRole('cell', { name: origKey }) })
|
||||||
.getByRole('cell', { name: 'Overflow Options' })
|
.getByRole('button', { name: 'Overflow Options' })
|
||||||
.click();
|
.click();
|
||||||
await this.page.getByRole('button', { name: 'Edit Tag' }).click();
|
await this.page.getByRole('button', { name: 'Edit Tag' }).click();
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export class WorkersPage extends BaseResourcePage {
|
|||||||
await this.page
|
await this.page
|
||||||
.getByRole('row')
|
.getByRole('row')
|
||||||
.filter({ has: this.page.getByRole('cell', { name: key }) })
|
.filter({ has: this.page.getByRole('cell', { name: key }) })
|
||||||
.getByRole('cell', { name: 'Overflow Options' })
|
.getByRole('button', { name: 'Overflow Options' })
|
||||||
.click();
|
.click();
|
||||||
await this.page.getByRole('button', { name: 'Remove Tag' }).click();
|
await this.page.getByRole('button', { name: 'Remove Tag' }).click();
|
||||||
|
|
||||||
|
|||||||
@@ -46,10 +46,11 @@ test.describe('Aliases (Enterprise)', () => {
|
|||||||
await projectsPage.createProject();
|
await projectsPage.createProject();
|
||||||
|
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createSshTargetWithAddressEnt(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'ssh',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
);
|
address: targetAddress,
|
||||||
|
});
|
||||||
const credentialStoresPage = new CredentialStoresPage(page);
|
const credentialStoresPage = new CredentialStoresPage(page);
|
||||||
await credentialStoresPage.createStaticCredentialStore();
|
await credentialStoresPage.createStaticCredentialStore();
|
||||||
const credentialName =
|
const credentialName =
|
||||||
@@ -165,12 +166,12 @@ test.describe('Aliases (Enterprise)', () => {
|
|||||||
await projectsPage.createProject();
|
await projectsPage.createProject();
|
||||||
alias = 'example.alias.' + nanoid();
|
alias = 'example.alias.' + nanoid();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createTargetWithAddressAndAlias(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'ssh',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
|
address: targetAddress,
|
||||||
alias,
|
alias,
|
||||||
'ssh',
|
});
|
||||||
);
|
|
||||||
const credentialStoresPage = new CredentialStoresPage(page);
|
const credentialStoresPage = new CredentialStoresPage(page);
|
||||||
await credentialStoresPage.createStaticCredentialStore();
|
await credentialStoresPage.createStaticCredentialStore();
|
||||||
const credentialName =
|
const credentialName =
|
||||||
@@ -236,10 +237,11 @@ test.describe('Aliases (Enterprise)', () => {
|
|||||||
const projectsPage = new ProjectsPage(page);
|
const projectsPage = new ProjectsPage(page);
|
||||||
const projectName = await projectsPage.createProject();
|
const projectName = await projectsPage.createProject();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createSshTargetWithAddressEnt(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'ssh',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
);
|
address: targetAddress,
|
||||||
|
});
|
||||||
const credentialStoresPage = new CredentialStoresPage(page);
|
const credentialStoresPage = new CredentialStoresPage(page);
|
||||||
await credentialStoresPage.createStaticCredentialStore();
|
await credentialStoresPage.createStaticCredentialStore();
|
||||||
const credentialName =
|
const credentialName =
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ test.describe('Aliases', () => {
|
|||||||
const projectsPage = new ProjectsPage(page);
|
const projectsPage = new ProjectsPage(page);
|
||||||
await projectsPage.createProject();
|
await projectsPage.createProject();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createTargetWithAddress(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
port: targetPort,
|
||||||
targetPort,
|
address: targetAddress,
|
||||||
);
|
});
|
||||||
|
|
||||||
// Create alias for target
|
// Create alias for target
|
||||||
const aliasName = 'Alias ' + nanoid();
|
const aliasName = 'Alias ' + nanoid();
|
||||||
@@ -148,11 +148,11 @@ test.describe('Aliases', () => {
|
|||||||
await projectsPage.createProject();
|
await projectsPage.createProject();
|
||||||
alias = 'example.alias.' + nanoid();
|
alias = 'example.alias.' + nanoid();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createTargetWithAddressAndAlias(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
port: targetPort,
|
||||||
targetPort,
|
address: targetAddress,
|
||||||
alias,
|
alias,
|
||||||
);
|
});
|
||||||
|
|
||||||
// Connect to target using alias
|
// Connect to target using alias
|
||||||
connect = await boundaryCli.connectToAlias(alias, sshUser, sshKeyPath);
|
connect = await boundaryCli.connectToAlias(alias, sshUser, sshKeyPath);
|
||||||
@@ -206,10 +206,10 @@ test.describe('Aliases', () => {
|
|||||||
const projectsPage = new ProjectsPage(page);
|
const projectsPage = new ProjectsPage(page);
|
||||||
const projectName = await projectsPage.createProject();
|
const projectName = await projectsPage.createProject();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createTargetWithAddress(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
port: targetPort,
|
||||||
targetPort,
|
address: targetAddress,
|
||||||
);
|
});
|
||||||
|
|
||||||
// Create new alias from scope page
|
// Create new alias from scope page
|
||||||
await boundaryCli.authenticateBoundary(
|
await boundaryCli.authenticateBoundary(
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ test(
|
|||||||
.getByRole('navigation', { name: 'Application local navigation' })
|
.getByRole('navigation', { name: 'Application local navigation' })
|
||||||
.getByRole('link', { name: 'Auth Methods' })
|
.getByRole('link', { name: 'Auth Methods' })
|
||||||
.click();
|
.click();
|
||||||
await page.getByRole('button', { name: 'New' }).click();
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Auth Methods' }),
|
||||||
|
).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'New', exact: true }).click();
|
||||||
await page.getByRole('link', { name: 'LDAP' }).click();
|
await page.getByRole('link', { name: 'LDAP' }).click();
|
||||||
|
|
||||||
const ldapAuthMethodName = 'LDAP ' + nanoid();
|
const ldapAuthMethodName = 'LDAP ' + nanoid();
|
||||||
|
|||||||
@@ -133,9 +133,7 @@ test(
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Log back in as an admin
|
// Log back in as an admin
|
||||||
await page.getByRole('button', { name: 'User Menu' }).click();
|
await loginPage.logout(email);
|
||||||
await page.getByRole('button', { name: 'Sign Out' }).click();
|
|
||||||
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible();
|
|
||||||
await loginPage.login(adminLoginName, adminPassword);
|
await loginPage.login(adminLoginName, adminPassword);
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('navigation', { name: 'breadcrumbs' }).getByText('Orgs'),
|
page.getByRole('navigation', { name: 'breadcrumbs' }).getByText('Orgs'),
|
||||||
|
|||||||
@@ -41,10 +41,11 @@ test(
|
|||||||
const projectsPage = new ProjectsPage(page);
|
const projectsPage = new ProjectsPage(page);
|
||||||
const projectName = await projectsPage.createProject();
|
const projectName = await projectsPage.createProject();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createSshTargetWithAddressEnt(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'ssh',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
);
|
address: targetAddress,
|
||||||
|
});
|
||||||
const credentialStoresPage = new CredentialStoresPage(page);
|
const credentialStoresPage = new CredentialStoresPage(page);
|
||||||
await credentialStoresPage.createStaticCredentialStore();
|
await credentialStoresPage.createStaticCredentialStore();
|
||||||
const credentialName =
|
const credentialName =
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ test.beforeEach(async ({ page, targetAddress, targetPort }) => {
|
|||||||
const projectsPage = new ProjectsPage(page);
|
const projectsPage = new ProjectsPage(page);
|
||||||
projectName = await projectsPage.createProject();
|
projectName = await projectsPage.createProject();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
targetName = await targetsPage.createTargetWithAddress(
|
targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
port: targetPort,
|
||||||
targetPort,
|
address: targetAddress,
|
||||||
);
|
});
|
||||||
const credentialStoresPage = new CredentialStoresPage(page);
|
const credentialStoresPage = new CredentialStoresPage(page);
|
||||||
await credentialStoresPage.createStaticCredentialStore();
|
await credentialStoresPage.createStaticCredentialStore();
|
||||||
});
|
});
|
||||||
@@ -191,12 +191,10 @@ test(
|
|||||||
.getByRole('link', { name: 'Credentials', exact: true })
|
.getByRole('link', { name: 'Credentials', exact: true })
|
||||||
.click();
|
.click();
|
||||||
await page.getByRole('link', { name: 'New', exact: true }).click();
|
await page.getByRole('link', { name: 'New', exact: true }).click();
|
||||||
await page.getByLabel('Name (Optional)').fill(credentialName);
|
await page.getByLabel('Name', { exact: true }).fill(credentialName);
|
||||||
await page.getByLabel('Description').fill('This is an automated test');
|
await page.getByLabel('Description').fill('This is an automated test');
|
||||||
await page
|
await page.getByRole('combobox', { name: 'Type' }).click();
|
||||||
.getByRole('group', { name: 'Type' })
|
await page.getByRole('option', { name: 'JSON' }).click();
|
||||||
.getByLabel('JSON')
|
|
||||||
.click();
|
|
||||||
await page.getByText('{}').click();
|
await page.getByText('{}').click();
|
||||||
const testName = 'name-json';
|
const testName = 'name-json';
|
||||||
const testPassword = 'password-json';
|
const testPassword = 'password-json';
|
||||||
|
|||||||
@@ -68,10 +68,11 @@ test(
|
|||||||
const projectsPage = new ProjectsPage(page);
|
const projectsPage = new ProjectsPage(page);
|
||||||
const projectName = await projectsPage.createProject();
|
const projectName = await projectsPage.createProject();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createSshTargetWithAddressEnt(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'ssh',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
);
|
address: targetAddress,
|
||||||
|
});
|
||||||
const credentialStoresPage = new CredentialStoresPage(page);
|
const credentialStoresPage = new CredentialStoresPage(page);
|
||||||
await credentialStoresPage.createVaultCredentialStoreWithWorkerFilter(
|
await credentialStoresPage.createVaultCredentialStoreWithWorkerFilter(
|
||||||
vaultAddrPrivate,
|
vaultAddrPrivate,
|
||||||
|
|||||||
@@ -85,10 +85,10 @@ test(
|
|||||||
const projectsPage = new ProjectsPage(page);
|
const projectsPage = new ProjectsPage(page);
|
||||||
const projectName = await projectsPage.createProject();
|
const projectName = await projectsPage.createProject();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createTargetWithAddress(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
port: targetPort,
|
||||||
targetPort,
|
address: targetAddress,
|
||||||
);
|
});
|
||||||
const credentialStoresPage = new CredentialStoresPage(page);
|
const credentialStoresPage = new CredentialStoresPage(page);
|
||||||
await credentialStoresPage.createVaultCredentialStore(
|
await credentialStoresPage.createVaultCredentialStore(
|
||||||
vaultAddr,
|
vaultAddr,
|
||||||
@@ -99,11 +99,9 @@ test(
|
|||||||
await page.getByRole('link', { name: 'Credential Libraries' }).click();
|
await page.getByRole('link', { name: 'Credential Libraries' }).click();
|
||||||
await page.getByRole('link', { name: 'New', exact: true }).click();
|
await page.getByRole('link', { name: 'New', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.getByLabel('Name (Optional)', { exact: true })
|
.getByLabel('Name', { exact: true })
|
||||||
.fill(credentialLibraryName);
|
.fill(credentialLibraryName);
|
||||||
await page
|
await page.getByLabel('Description').fill('This is an automated test');
|
||||||
.getByLabel('Description (Optional)')
|
|
||||||
.fill('This is an automated test');
|
|
||||||
await page
|
await page
|
||||||
.getByLabel('Vault Path')
|
.getByLabel('Vault Path')
|
||||||
.fill(`${secretsPath}/data/${secretName}`);
|
.fill(`${secretsPath}/data/${secretName}`);
|
||||||
|
|||||||
@@ -156,7 +156,10 @@ test.describe('AWS', () => {
|
|||||||
|
|
||||||
// Create a target and add DHC host set as a host source
|
// Create a target and add DHC host set as a host source
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createSshTargetEnt(targetPort);
|
const targetName = await targetsPage.createTarget({
|
||||||
|
targetType: 'ssh',
|
||||||
|
port: targetPort,
|
||||||
|
});
|
||||||
await targetsPage.addHostSourceToTarget(hostSetName);
|
await targetsPage.addHostSourceToTarget(hostSetName);
|
||||||
|
|
||||||
// Add another host source
|
// Add another host source
|
||||||
|
|||||||
@@ -156,7 +156,9 @@ test.describe('AWS', () => {
|
|||||||
|
|
||||||
// Create a target and add DHC host set as a host source
|
// Create a target and add DHC host set as a host source
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createTarget(targetPort);
|
const targetName = await targetsPage.createTarget({
|
||||||
|
port: targetPort,
|
||||||
|
});
|
||||||
await targetsPage.addHostSourceToTarget(hostSetName);
|
await targetsPage.addHostSourceToTarget(hostSetName);
|
||||||
|
|
||||||
// Add another host source
|
// Add another host source
|
||||||
|
|||||||
@@ -93,10 +93,11 @@ test(
|
|||||||
await page.getByRole('link', { name: orgName }).click();
|
await page.getByRole('link', { name: orgName }).click();
|
||||||
await page.getByRole('link', { name: projectName }).click();
|
await page.getByRole('link', { name: projectName }).click();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createSshTargetWithAddressEnt(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'ssh',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
);
|
address: targetAddress,
|
||||||
|
});
|
||||||
await targetsPage.addEgressWorkerFilterToTarget(
|
await targetsPage.addEgressWorkerFilterToTarget(
|
||||||
`"${workerTagEgress}" in "/tags/type"`,
|
`"${workerTagEgress}" in "/tags/type"`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -97,10 +97,11 @@ test(
|
|||||||
await page.getByRole('link', { name: orgName }).click();
|
await page.getByRole('link', { name: orgName }).click();
|
||||||
await page.getByRole('link', { name: projectName }).click();
|
await page.getByRole('link', { name: projectName }).click();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createSshTargetWithAddressEnt(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'ssh',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
);
|
address: targetAddress,
|
||||||
|
});
|
||||||
await targetsPage.addIngressWorkerFilterToTarget(
|
await targetsPage.addIngressWorkerFilterToTarget(
|
||||||
`"${workerTagEgress}" in "/tags/type"`,
|
`"${workerTagEgress}" in "/tags/type"`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -99,10 +99,11 @@ test(
|
|||||||
|
|
||||||
// Create target
|
// Create target
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createSshTargetWithAddressEnt(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'ssh',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
);
|
address: targetAddress,
|
||||||
|
});
|
||||||
|
|
||||||
// Create credentials
|
// Create credentials
|
||||||
const credentialStoresPage = new CredentialStoresPage(page);
|
const credentialStoresPage = new CredentialStoresPage(page);
|
||||||
|
|||||||
@@ -89,10 +89,11 @@ test(
|
|||||||
|
|
||||||
// Create target
|
// Create target
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createSshTargetWithAddressEnt(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'ssh',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
);
|
address: targetAddress,
|
||||||
|
});
|
||||||
|
|
||||||
// Create credentials
|
// Create credentials
|
||||||
const credentialStoresPage = new CredentialStoresPage(page);
|
const credentialStoresPage = new CredentialStoresPage(page);
|
||||||
|
|||||||
@@ -41,10 +41,11 @@ test(
|
|||||||
const projectsPage = new ProjectsPage(page);
|
const projectsPage = new ProjectsPage(page);
|
||||||
const projectName = await projectsPage.createProject();
|
const projectName = await projectsPage.createProject();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createTcpTargetWithAddressEnt(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'tcp',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
);
|
address: targetAddress,
|
||||||
|
});
|
||||||
|
|
||||||
await boundaryCli.authenticateBoundary(
|
await boundaryCli.authenticateBoundary(
|
||||||
controllerAddr,
|
controllerAddr,
|
||||||
@@ -108,10 +109,11 @@ test(
|
|||||||
const projectsPage = new ProjectsPage(page);
|
const projectsPage = new ProjectsPage(page);
|
||||||
const projectName = await projectsPage.createProject();
|
const projectName = await projectsPage.createProject();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createSshTargetWithAddressEnt(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'ssh',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
);
|
address: targetAddress,
|
||||||
|
});
|
||||||
const credentialStoresPage = new CredentialStoresPage(page);
|
const credentialStoresPage = new CredentialStoresPage(page);
|
||||||
await credentialStoresPage.createStaticCredentialStore();
|
await credentialStoresPage.createStaticCredentialStore();
|
||||||
const credentialName =
|
const credentialName =
|
||||||
@@ -194,7 +196,10 @@ test(
|
|||||||
|
|
||||||
// Create target
|
// Create target
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createSshTargetEnt(targetPort);
|
const targetName = await targetsPage.createTarget({
|
||||||
|
targetType: 'ssh',
|
||||||
|
port: targetPort,
|
||||||
|
});
|
||||||
await targetsPage.addHostSourceToTarget(hostSetName);
|
await targetsPage.addHostSourceToTarget(hostSetName);
|
||||||
|
|
||||||
// Add/Remove another host source
|
// Add/Remove another host source
|
||||||
@@ -285,10 +290,11 @@ test(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const rdpTarget = await targetsPage.createRDPTargetWithAddressEnt(
|
const rdpTarget = await targetsPage.createTarget({
|
||||||
targetAddress,
|
targetType: 'rdp',
|
||||||
targetPort,
|
port: targetPort,
|
||||||
);
|
address: targetAddress,
|
||||||
|
});
|
||||||
await targetsPage.addBrokeredCredentialsToTarget(
|
await targetsPage.addBrokeredCredentialsToTarget(
|
||||||
rdpTarget,
|
rdpTarget,
|
||||||
credentialName,
|
credentialName,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ test(
|
|||||||
|
|
||||||
// Create target
|
// Create target
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createTarget(targetPort);
|
const targetName = await targetsPage.createTarget({ port: targetPort });
|
||||||
await targetsPage.addHostSourceToTarget(hostSetName);
|
await targetsPage.addHostSourceToTarget(hostSetName);
|
||||||
|
|
||||||
// Add/Remove another host source
|
// Add/Remove another host source
|
||||||
@@ -127,10 +127,10 @@ test(
|
|||||||
const projectsPage = new ProjectsPage(page);
|
const projectsPage = new ProjectsPage(page);
|
||||||
const projectName = await projectsPage.createProject();
|
const projectName = await projectsPage.createProject();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
const targetName = await targetsPage.createTargetWithAddress(
|
const targetName = await targetsPage.createTarget({
|
||||||
targetAddress,
|
port: targetPort,
|
||||||
targetPort,
|
address: targetAddress,
|
||||||
);
|
});
|
||||||
|
|
||||||
await boundaryCli.authenticateBoundary(
|
await boundaryCli.authenticateBoundary(
|
||||||
controllerAddr,
|
controllerAddr,
|
||||||
@@ -191,7 +191,10 @@ test(
|
|||||||
const projectsPage = new ProjectsPage(page);
|
const projectsPage = new ProjectsPage(page);
|
||||||
await projectsPage.createProject();
|
await projectsPage.createProject();
|
||||||
const targetsPage = new TargetsPage(page);
|
const targetsPage = new TargetsPage(page);
|
||||||
await targetsPage.createTargetWithAddress(targetAddress, targetPort);
|
await targetsPage.createTarget({
|
||||||
|
port: targetPort,
|
||||||
|
address: targetAddress,
|
||||||
|
});
|
||||||
|
|
||||||
// Update target
|
// Update target
|
||||||
await page.getByRole('button', { name: 'Edit Form' }).click();
|
await page.getByRole('button', { name: 'Edit Form' }).click();
|
||||||
@@ -234,7 +237,9 @@ test(
|
|||||||
.locator('.CodeMirror')
|
.locator('.CodeMirror')
|
||||||
.getByRole('textbox')
|
.getByRole('textbox')
|
||||||
.click({ force: true });
|
.click({ force: true });
|
||||||
await page.keyboard.press('Meta+A');
|
const selectAllShortcut =
|
||||||
|
process.platform === 'darwin' ? 'Meta+A' : 'Control+A';
|
||||||
|
await page.keyboard.press(selectAllShortcut);
|
||||||
await page.keyboard.press('Backspace');
|
await page.keyboard.press('Backspace');
|
||||||
await page
|
await page
|
||||||
.locator('.CodeMirror')
|
.locator('.CodeMirror')
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
import { expect, test } from '../fixtures/baseTest.js';
|
import { expect, test } from '../fixtures/baseTest.js';
|
||||||
import * as boundaryHttp from '../../helpers/boundary-http.js';
|
import * as boundaryHttp from '../../helpers/boundary-http.js';
|
||||||
import { textToMatch } from '../fixtures/tesseractTest.js';
|
import { textToMatch } from '../fixtures/tesseractTest.js';
|
||||||
|
import {
|
||||||
|
isRdpClientInstalled,
|
||||||
|
isRdpRunning,
|
||||||
|
killRdpProcesses,
|
||||||
|
isOSForRdpSupported,
|
||||||
|
} from '../../helpers/rdp.js';
|
||||||
|
|
||||||
const hostName = 'Host name for test';
|
const hostName = 'Host name for test';
|
||||||
let org;
|
let org;
|
||||||
@@ -117,7 +123,6 @@ test.beforeEach(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create an RDP target and add host source and credential sources
|
// Create an RDP target and add host source and credential sources
|
||||||
// TODO: A test for RDP target connection will be added later when the Proxy is in place.
|
|
||||||
rdpTarget = await boundaryHttp.createTarget(request, {
|
rdpTarget = await boundaryHttp.createTarget(request, {
|
||||||
scopeId: project.id,
|
scopeId: project.id,
|
||||||
type: 'rdp',
|
type: 'rdp',
|
||||||
@@ -285,4 +290,37 @@ test.describe('Targets tests', () => {
|
|||||||
authedPage.getByRole('link', { name: targetWithHost.name }),
|
authedPage.getByRole('link', { name: targetWithHost.name }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Launches RDP client when connecting to an RDP target', async ({
|
||||||
|
authedPage,
|
||||||
|
}) => {
|
||||||
|
const isRdpClientInstalledCheck = await isRdpClientInstalled();
|
||||||
|
const isOSForRdpSupportedCheck = await isOSForRdpSupported();
|
||||||
|
|
||||||
|
test.skip(
|
||||||
|
!isRdpClientInstalledCheck || !isOSForRdpSupportedCheck,
|
||||||
|
'RDP client is not installed/supported on this system',
|
||||||
|
);
|
||||||
|
|
||||||
|
const beforeLaunchRunning = await isRdpRunning();
|
||||||
|
expect(beforeLaunchRunning).toBe(false);
|
||||||
|
|
||||||
|
await authedPage.getByRole('link', { name: rdpTarget.name }).click();
|
||||||
|
await authedPage.getByRole('button', { name: 'Open' }).click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
authedPage.getByRole('heading', { name: 'Sessions' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const afterLaunchRunning = await isRdpRunning();
|
||||||
|
expect(afterLaunchRunning).toBe(true);
|
||||||
|
|
||||||
|
await authedPage.getByRole('button', { name: 'End Session' }).click();
|
||||||
|
await expect(authedPage.getByText('Canceled successfully.')).toBeVisible();
|
||||||
|
|
||||||
|
killRdpProcesses();
|
||||||
|
|
||||||
|
const afterKillRunning = await isRdpRunning();
|
||||||
|
expect(afterKillRunning).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
|
||||||
|
export async function isOSForRdpSupported() {
|
||||||
|
return process.platform === 'win32' || process.platform === 'darwin';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isRdpRunning() {
|
||||||
|
try {
|
||||||
|
let result;
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
result = execSync('tasklist /FI "IMAGENAME eq mstsc.exe" /FO CSV /NH', {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
return result && result.includes('mstsc.exe');
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
result = execSync('pgrep -x "Windows App"', {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
return result && result.trim().length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isRdpClientInstalled() {
|
||||||
|
try {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
execSync('where mstsc', { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
const result = execSync(
|
||||||
|
'mdfind "kMDItemCFBundleIdentifier == \'com.microsoft.rdc.macos\'"',
|
||||||
|
{ encoding: 'utf-8' },
|
||||||
|
);
|
||||||
|
return result && result.trim().length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function killRdpProcesses() {
|
||||||
|
try {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
execSync('taskkill /F /IM mstsc.exe', { stdio: 'ignore' });
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
execSync('pkill -x "Windows App"', { stdio: 'ignore' });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.10",
|
"@babel/core": "^7.26.10",
|
||||||
"@babel/eslint-parser": "^7.21.3",
|
"@babel/eslint-parser": "^7.21.3",
|
||||||
"@openapitools/openapi-generator-cli": "^2.23.4",
|
"@openapitools/openapi-generator-cli": "^2.25.2",
|
||||||
"@playwright/test": "^1.49.0",
|
"@playwright/test": "^1.56.1",
|
||||||
"concurrently": "^9.1.0",
|
"concurrently": "^9.1.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"eslint": "^8.54.0",
|
"eslint": "^8.54.0",
|
||||||
|
|||||||
Loaded 100 of 367 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user