Compare commits

..
59 Commits
Author SHA1 Message Date
hashicc cd3fc5cea7 fix: 🐛 apply .sr-only fix to all instances 2025-12-11 10:08:17 -08:00
Dharini 719bab7358 chore: 🤖 Desktop client changelog and cli version update (#3087) 2025-11-21 13:29:01 -08:00
Zhihe Li 95f72e1248 chore: 🤖 Update playwright dependency (#3085)
* chore: 🤖 Update playwright dependency

* chore: 🤖 Update openapi generator
2025-11-20 12:22:16 -05:00
hashicc f9a9f677ca chore: 🤖 use statically defined credential store components (#3071)
* chore: 🤖 use statically defined cred store components

* Add missing copyright headers
2025-11-20 10:19:46 -05:00
Cameron Perera dd0c5df25c chore: 🤖 update tmp package to 0.2.5 (#3026)
* chore: 🤖 update tmp package to 0.2.5

* chore: 🤖 add tmp@0.2.5 override
2025-11-19 17:49:45 -06:00
Priya Patel 4fc3244049 refactor: 💡 update logic to get preferred rdp client (#3066)
* refactor: 💡 updated logic to store preferred rdp client

* refactor: 💡 simplified preferred client logic
2025-11-19 14:22:07 -08:00
Priya Patel 5faf830d46 refactor: 💡 updated magic strings with const (#3064) 2025-11-19 14:22:07 -08:00
Priya Patel d2cf82b85c e2e Rdp client tests (#3055)
* test: 💍 e2e test for rdp client launch

* test: 💍 added rdp test helper

* refactor: 💡 misc

* refactor: 💡 addressed comments

* refactor: 💡 removed similar test
2025-11-19 14:22:07 -08:00
Priya Patel fb0020c395 feat: 🎸 Add RDP client launch on session details page (#3037)
* feat: 🎸 preferred clients section on settings page

* refactor: 💡 moved rdp calls to app route

* test: 💍 fixed failings tests

* refactor: 💡 addressed comments

* fix: 🐛 fixed failing tests

* refactor: 💡 add mock rdp service calls to tests

* feat: 🎸 adds open button to session details page

* fix: 🐛 translation string

* refactor: 💡 address feedback

* refactor: 💡 address comment

* fix: 🐛 test failure
2025-11-19 14:22:07 -08:00
Priya Patel e55ea0b629 feat: 🎸 preferred clients section on settings page (#3034)
* feat: 🎸 preferred clients section on settings page

* chore: 🤖 update mock ipc with new handlers

* refactor: 💡 moved windows rdp protocol to const

* refactor: 💡 moved rdp calls to app route

* test: 💍 fixed failings tests

* refactor: 💡 addressed comments

* fix: 🐛 fixed failing tests

* refactor: 💡 add mock rdp service calls to tests

* refactor: 💡 addressed comments

* test: 💍 add additional tests

* refactor: 💡 fixed test failure
2025-11-19 14:22:06 -08:00
Dharini 9234dbae70 Icu 17881 UI desktop add rdp launch button in target detail view (#3038) 2025-11-19 14:22:06 -08:00
Dharini 27136eaa71 feat: Add launch functionality in targets list view (#3035) 2025-11-19 14:22:06 -08:00
Dharini 6554af1164 chore: 🤖 add ipc handlers for rdp (#3031) 2025-11-19 14:22:06 -08:00
Priya Patel 437d0e686d Create new RDP service (#3029)
* feat: 🎸 added a new RDP service

* fix: 🐛 rdp test

* refactor: 💡 update error handling for fetching rdp clients

* refactor: 💡 addressed comments

* fix: 🐛 revert private field

* refactor: 💡 addressed comments

* refactor: 💡 updated rdp clients to array of strings
2025-11-19 14:22:06 -08:00
Michael Li 25b801826d chore(e2e): Fix slack message (#3079) 2025-11-19 16:52:14 -05:00
Michael Li 123818dc70 chore(e2e): Update workflow triggers for e2e tests (#3077)
* chore(e2e): Update workflow triggers for e2e tests

* CR
2025-11-18 18:27:30 -05:00
Michael Li dafcd046d7 fix(e2e): Improve locator to avoid finding multiple elements (#3076) 2025-11-18 17:00:45 -05:00
Lisbet Alvarez accf9faf60 refactor: 💡 update credential-library form for password type (#3060)
* refactor: 💡 update credential-library form for password type

* style: 💄 fix display issue

* test: 💍 add test cases
2025-11-18 11:20:49 -08:00
Lisbet Alvarez 6bb237e78c Add password credential form (#3054)
* feat: 🎸 add password credential form

* test: 💍 add a11y ignores

* test: 💍 fix e2e tests

* test: 💍 add password cred type to targets brokering tests

* chore: 🤖 remove comment
2025-11-18 11:20:49 -08:00
Lisbet Alvarez bbb5808875 chore: 🤖 clean-up json-credentials feature flag (#3049) 2025-11-18 11:20:49 -08:00
Lisbet Alvarez 905a8f1951 Refactor cred type radio field (#3046)
* refactor: 💡 credential type radio field to be select field

* refactor: 💡 radio component to be select component

* test: 💍 fix e2e test page

* test: 💍 add correct selector to e2e test

* test: 💍 fix
2025-11-18 11:20:49 -08:00
Lisbet Alvarez f39961bbf5 Add password type to models and mirage (#3039)
* feat: 🎸 add new password type to cred models + mirage

* test: 💍 fix

* refactor: 💡 update target mirage trait

* chore: 🤖 add missing attr type
2025-11-18 11:20:49 -08:00
Michael Li 2bf09011fb fix(e2e): Fix LDAP test when org name contains the word "New" (#3074)
* fix(e2e): Wait for auth methods page to load

* fix(e2e): Make search for New button exact
2025-11-18 08:35:46 -05:00
Zhihe Li 013aa2e570 feat: 🎸 Refactored sessions page filters to load separately (#3053) 2025-11-17 17:06:47 -05:00
Michael Li 953e35bcd6 chore(e2e): Add slack message on e2e test failure (#3070)
* chore(e2e): Add slack message on test failure

* chore(e2e): Remove deprecated variable

* CR: only run in boundary-ui
2025-11-17 15:34:13 -05:00
hashicc e138b06fd3 chore: 🤖 remove leftover .yarn file (#3068) 2025-11-17 13:43:56 -05:00
hashicc 2ef6b0549d chore: 🤖 hoist esbuild to consolidate ember-source dependencies (#3052)
* chore: 🤖 hoist esbuild to fix dependency resolutions

* chore: 🤖 add back esbuild as an explicit dep for api addon

This addresses an eslint error but also is more explicit
with the api addon using esbuild directly
2025-11-13 13:32:08 -05:00
hashicc 1c49303432 chore: 🤖 update electron to avoid potential macos tahoe issue (#3024)
Electron has an issue being reported with latest macos tahoe:
https://github.com/electron/electron/issues/48311

This has been fixed in:
38.2.0
37.6.0
36.9.2

We are currently on 38.0.0 so this commit bumps electron to 38.2.0
2025-11-13 08:38:57 -05:00
hashicc f31b1e975d chore: 🤖 bump hds dependency (#3059) 2025-11-12 16:43:38 -05:00
Lisbet Alvarez 48d0087141 Unload expired sessions from store (#3033)
* fix: 🐛 unload expired sessions from ember data store

* fix: 🐛 add unloadAll & include terminated to admin ui

* test: 💍 update tests

* refactor: 💡 pr feedback for admin ui

* refactor: 💡 remove unneeded query from desktop ui

* refactor: 💡 check pushToStore var

* Revert "refactor: 💡 remove unneeded query from desktop ui"

* fix: 🐛 fix proxy details being removed after unload

* refactor: 💡 remove log
2025-11-11 11:16:12 -08:00
Zhihe Li 58cc9e56fc fix: 🐛 Fix filters sometimes not being displayed (#3050)
One of the changes that was made was not retrieving all resources for the dropdown filters. 
This had an unintended consequence when displaying the selected filters as it's possible for the selected filter to not be in the list of allFilters.

There's two main scenarios this happens (the prerequisite is having more filter options than we display which is 250):

* When filtering down the dropdown filters which means it might not be in the allFilters options. This was fixed by keeping track of all the searched options to be able to display.
* A user uses a query string or bookmarked a URL and goes to it directly. If the option isn't in the initial load, we won't have the actual option. In this case, I changed it to at least just display the ID instead of not showing up at all. Otherwise I'd have to load these selected options separately from the initial load which doesn't seem worth for this edge case.
2025-11-11 11:57:04 -05:00
Dylan HyunandZhihe Li 189a453e8b style: 💄 align typography and border styles to HDS tokens (#3051)
* Feat: Convert font-family styles to HDS tokens

* Feat: Convert font-size styles to HDS tokens

* Feat: Convert font-weight styles to HDS tokens

* Feat: Convert border-radius styles to HDS tokens

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2025-11-07 12:32:54 -05:00
Lisbet Alvarez 6fca90747c chore: 🤖 Add ignores for new violations (#3047) 2025-11-06 13:23:33 -08:00
Zhihe Li 92f5a65c00 feat: 🎸 Add ability to search other resources as well (#3021)
This commit adds the ability to search across multiple tables with joins which unlocks a better search experience in the case of tables that source their data from multiple tables.
2025-11-05 17:57:45 -05:00
Priya Patel d8744e81bc refactor: 💡 removed vault_ldap_feature flag (#3045) 2025-11-05 14:03:09 -06:00
dependabot[bot]andLisbet Alvarez 136baf8260 chore(deps): bump pnpm/action-setup from 4.1.0 to 4.2.0 (#3044)
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/a7487c7e89a18df4991f7f222e4898a00d66ddda...41ff72655975bd51cab0327fa583b6e92b6d3061)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
2025-11-04 13:35:58 -08:00
dependabot[bot]andLisbet Alvarez ef47c163d0 chore(deps): bump actions/upload-artifact from 4.6.2 to 5.0.0 (#3043)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 5.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...330a01c490aca151604b8cf639adc76d48f6c5d4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
2025-11-04 13:23:43 -08:00
dependabot[bot]andLisbet Alvarez 39e5e3406b chore(deps): bump stefanzweifel/git-auto-commit-action (#3042)
Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 6.0.1 to 7.0.0.
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/778341af668090896ca464160c2def5d1d1a3eb0...28e16e81777b558cc906c8750092100bbb34c5e3)

---
updated-dependencies:
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
2025-11-04 13:06:57 -08:00
dependabot[bot]andLisbet Alvarez 91d8d61afb chore(deps): bump hashicorp/action-setup-enos from 1.37 to 1.38 (#3041)
Bumps [hashicorp/action-setup-enos](https://github.com/hashicorp/action-setup-enos) from 1.37 to 1.38.
- [Commits](https://github.com/hashicorp/action-setup-enos/compare/v1.37...39a8f388434bea5eeba6649b99aa9949a55a1150)

---
updated-dependencies:
- dependency-name: hashicorp/action-setup-enos
  dependency-version: '1.38'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
2025-11-04 12:41:38 -08:00
dependabot[bot]andLisbet Alvarez b1d9153efe chore(deps): bump actions/setup-node from 4.4.0 to 6.0.0 (#3040)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.4.0...2028fbc5c25fe9cf00d9f06a71cc4710d4507903)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
2025-11-04 12:14:10 -08:00
Zhihe Li fda1f56ad1 feat: 🎸 Add support for joins with filters (#3010)
This commit adds support for being able to join to other tables when filtering.
Support was also added for being able to find null values.
2025-11-03 17:28:11 -05:00
Zhihe Li fa18bfff01 feat: 🎸 Refactor session recording filters (#2981)
This commit has a few notable changes:

1. A simple attempt at establishing a pattern for using SELECT statements. This will be useful in the future for joins/subquery support.
2. Expanded usage of searching to be able to pass instead of just a string, an object that lets a consumer choose which columns to search on.
3. Decouple our session recording filters from being loaded from the route model hook which is a departure from our convention. This has the benefit of actually letting the page first load and having the filters load async so the page doesn't wait for the filters to finish. This is also much faster to query for each individual filter option from SQLite rather than returning all data to filter in application code.
2025-10-31 16:58:05 -04:00
Lisbet Alvarez 72f7c8a92d fix: 🐛 OIDC authentications not properly showing username (#2930)
* fix: 🐛 properly show username for oidc auth method in user menu

* test: 💍 fix admin ui tests

* test: 💍 fix desktop ui tests

* refactor: 💡 admin to not store username in authenticated object

* refactor: 💡 use getter in app controller

* test: 💍 POC for how we could update our tests to use auth account

* refactor: 💡 remove unused service

* test: 💍 fix all tests in ui/admin

* test: 💍 revert one change

* refactor: 💡 update ui/desktop w/ feedback + fix tests

* refactor: 💡 add in error handling

* refactor: 💡 cleanup unused service

* test: 💍 fix e2e test selector

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17231
2025-10-30 12:49:30 -07:00
Zhihe Li 838bc37243 fix: 🐛 Fix sanitizing of tokens when logging (#3036) 2025-10-29 17:05:22 -04:00
hashicc 866972c6c6 chore: 🤖 create patched ember-loading for modern e-c (#3015) 2025-10-28 10:04:32 -04:00
dillanb-hashi b19d76f249 chore: 🤖 Added github action to run e2e ui tests (#3025)
* chore: 🤖 Added github action to run e2e ui tests
2025-10-21 09:18:05 -07:00
Zhihe Li 35ccaeba56 style: 💄 Update if curly brace statements (#3023) 2025-10-13 10:53:21 -04:00
hashicc bf134072e5 refactor: 💡 migrate away from component helper for auth-method (#3011)
* refactor: 💡 migrate away from component helper for auth-method
component

* Add missing copyright headers
2025-10-08 12:53:19 -04:00
hashicc 0bd152e0b5 Migrate away from component helper in credential library form component (#3013)
* refactor: 💡 migrate away from component helper for cred library form

* Add missing copyright headers
2025-10-07 13:40:30 -04:00
dependabot[bot] 928ca5409c chore(deps): bump hashicorp/action-setup-enos from 1.37 to 1.38 (#3016)
Bumps [hashicorp/action-setup-enos](https://github.com/hashicorp/action-setup-enos) from 1.37 to 1.38.
- [Commits](https://github.com/hashicorp/action-setup-enos/compare/b9fa53484a1e8fdcc7b02a118bcf01d65b9414c9...39a8f388434bea5eeba6649b99aa9949a55a1150)

---
updated-dependencies:
- dependency-name: hashicorp/action-setup-enos
  dependency-version: '1.38'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 10:16:55 -04:00
dependabot[bot] 0f846eab2c chore(deps): bump actions/setup-node from 4.4.0 to 5.0.0 (#3017)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 5.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/49933ea5288caeca8642d1e84afbd3f7d6820020...a0853c24544627f65ddf259abe73b1d18a591444)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 21:25:12 -04:00
dependabot[bot]andLisbet Alvarez 14ed74dd3d chore(deps): bump actions/labeler from 5.0.0 to 6.0.1 (#3018)
Bumps [actions/labeler](https://github.com/actions/labeler) from 5.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/8558fd74291d67161a8a78ce36a881fa63b766a9...634933edcd8ababfe52f92936142cc22ac488b1b)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
2025-10-06 13:34:51 -07:00
Lisbet Alvarez 701d19bd18 chore: 🤖 resolve on-headers dependabot alert (#3020) 2025-10-06 13:17:50 -07:00
hashicc 240a544d82 refactor: 💡 remove dynamic component helper from form/authenticate (#3012)
* refactor: 💡 remove dynamic component helper from form/authenticate

* Add missing copyright headers
2025-10-06 15:09:42 -04:00
Priya Patel b4f3741979 fix: 🐛 Update vault-ldap cred library (#3000)
* fix: 🐛 remove UPD cred type from vault-ldap

* fix: 🐛 failing tests

* fix: 🐛 remove log
2025-10-02 12:08:25 -05:00
dependabot[bot]andZhihe Li 396bf0a43f chore(deps): bump browser-actions/setup-chrome from 1.7.3 to 2.1.0 (#2982)
Bumps [browser-actions/setup-chrome](https://github.com/browser-actions/setup-chrome) from 1.7.3 to 2.1.0.
- [Release notes](https://github.com/browser-actions/setup-chrome/releases)
- [Changelog](https://github.com/browser-actions/setup-chrome/blob/master/CHANGELOG.md)
- [Commits](https://github.com/browser-actions/setup-chrome/compare/v1.7.3...b94431e051d1c52dcbe9a7092a4f10f827795416)

---
updated-dependencies:
- dependency-name: browser-actions/setup-chrome
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2025-10-01 11:01:50 -04:00
dependabot[bot] 3225ccda10 chore(deps): bump actions/checkout from 4.2.2 to 5.0.0 (#2983)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 5.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/11bd71901bbe5b1630ceea73d27597364c9af683...08c6903cd8c0fde910a37f88322edcfb5dd907a8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 10:49:55 -04:00
Michael Li ad2c1ba2ad chore(e2e): Update desktop e2e test workflow to use new environment variable (#3014)
* chore(e2e): Update to use boundary license env

* chore(e2e): Allow runs if workflow_dispatch
2025-10-01 09:06:36 -04:00
dillanb-hashi ea591df0bb test: 💍 Refactor target functions and fix failing test (#2999)
* test: 💍 Fixed broken test 

* test: 💍 Refactoring to consolidate target function names

* test: 💍 fixed failing tests with capitalization issue
2025-09-30 09:35:26 -07:00
367 changed files with 5827 additions and 3767 deletions

No files matched your search

+5 -5
View File
@@ -13,14 +13,14 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }}
timeout-minutes: 40
steps:
- uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1.7.3
- uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0
with:
install-dependencies: true
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 20
cache: 'pnpm'
@@ -41,7 +41,7 @@ jobs:
- name: Output ember a11y test report summary (desktop)
run: 'cat ember-a11y-reports/desktop.json | jq "map({moduleName,testName,routes,urls,violations: .violations | map({id, nodes: .nodes | map({failureSummary, targets: .[].[]?.relatedNodes?.[]?.target? })}) })"'
- name: Upload ember a11y reports
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: ember-a11y-reports
path: ./ember-a11y-reports
+16 -16
View File
@@ -11,11 +11,11 @@ jobs:
name: Install dependencies
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 20
cache: 'pnpm'
@@ -26,12 +26,12 @@ jobs:
needs: [dependencies]
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Build
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 20
cache: 'pnpm'
@@ -39,12 +39,12 @@ jobs:
- run: pnpm install --offline
- run: pnpm run build:ui:admin:oss
- name: Upload artifact admin-ui
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: admin-ui
path: ./ui/admin/dist/
- name: Upload artifact OSS admin UI
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: admin-ui-oss
path: ./ui/admin/dist/
@@ -53,12 +53,12 @@ jobs:
needs: [dependencies]
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Build
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 20
cache: 'pnpm'
@@ -66,7 +66,7 @@ jobs:
- run: pnpm install --offline
- run: pnpm run build:ui:admin:enterprise
- name: Upload artifact enterprise admin UI
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: admin-ui-ent
path: ./ui/admin/dist/
@@ -75,12 +75,12 @@ jobs:
needs: [dependencies]
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Build
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 20
cache: 'pnpm'
@@ -88,7 +88,7 @@ jobs:
- run: pnpm install --offline
- run: pnpm run build:ui:admin:hcp
- name: Upload artifact HCP admin UI
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: admin-ui-hcp
path: ./ui/admin/dist/
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+2 -2
View File
@@ -6,7 +6,7 @@ jobs:
permissions:
contents: write
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
with:
ref: ${{ github.event.pull_request.head.ref }}
@@ -14,6 +14,6 @@ jobs:
- uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3
- name: Add missing Copyright 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:
commit_message: Add missing copyright headers
+1 -1
View File
@@ -6,6 +6,6 @@ jobs:
triage:
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
+12 -12
View File
@@ -6,11 +6,11 @@ jobs:
name: Install dependencies
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 20
cache: 'pnpm'
@@ -22,11 +22,11 @@ jobs:
needs: [dependencies]
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 20
cache: 'pnpm'
@@ -40,11 +40,11 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }}
timeout-minutes: 5
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 20
cache: 'pnpm'
@@ -61,11 +61,11 @@ jobs:
- uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0
with:
install-dependencies: true
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 20
cache: 'pnpm'
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
- run: |
+207
View File
@@ -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 }}"
+25 -12
View File
@@ -3,30 +3,35 @@ name: Test E2E Desktop Client
on:
pull_request:
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:
jobs:
test:
if: ${{contains(github.event.pull_request.labels.*.name, 'desktop')}}
name: Run Desktop E2E Tests
name: test
runs-on: ${{ fromJSON(vars.RUNNER_UBUNTU_22) }}
env:
ENOS_VAR_boundary_edition: "ent"
ENOS_VAR_e2e_debug_no_run: true
ENOS_VAR_boundary_license_path: "./license.hclic"
SETUP_CLI: true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Set up pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: 20
cache: "pnpm"
@@ -38,7 +43,7 @@ jobs:
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd
- name: Set up Enos
uses: hashicorp/action-setup-enos@b9fa53484a1e8fdcc7b02a118bcf01d65b9414c9
uses: hashicorp/action-setup-enos@39a8f388434bea5eeba6649b99aa9949a55a1150
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -48,7 +53,7 @@ jobs:
unzip /tmp/vault.zip -d /usr/local/bin
- name: Checkout boundary-enterprise
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
repository: hashicorp/boundary-enterprise
# ref: ${{ github.event.inputs.boundary-enterprise-branch }}
@@ -72,16 +77,14 @@ jobs:
docker pull hashicorp/boundary-enterprise:latest
docker save hashicorp/boundary-enterprise:latest | gzip > /tmp/boundary-enterprise.zip
# TODO: Set up license in a better way? Use directly rather than writing to file and pull from secrets manager
- name: Set up test infra
id: infra
run: |
echo "${{ secrets.BOUNDARY_ENT_LICENSE }}" > ./support/src/boundary-enterprise/enos/license.hclic
ssh-keygen -N '' -t ed25519 -f ~/.ssh/github_enos
mkdir -p ./enos/terraform-plugin-cache
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
export ENOS_VAR_boundary_docker_image_name=hashicorp/boundary-enterprise:latest && \
export ENOS_VAR_boundary_license="${{ secrets.BOUNDARY_ENT_LICENSE }}" && \
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
@@ -102,7 +105,7 @@ jobs:
- name: Upload Playwright report
if: ${{ failure() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: playwright-report
path: e2e-tests/playwright-report
@@ -113,3 +116,13 @@ jobs:
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
enos scenario destroy --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:crt
- 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 }}"
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
trigger-gha-release:
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Release description
run: |
echo "Triggering release with a TAG ${GITHUB_REF#refs/tags/}"
Binary file not shown.
+1
View File
@@ -26,6 +26,7 @@ module.exports = {
browser: true,
},
rules: {
curly: ['error', 'multi-line', 'consistent'],
'ember/no-get': 'off',
'ember/no-get-with-default': 'off',
'ember/no-computed-properties-in-native-classes': 'off',
+2 -1
View File
@@ -59,10 +59,11 @@ export default class RoleAbility extends ModelAbility {
const type = this.model.constructor.modelName;
const typeIsValid =
type === 'user' || type === 'group' || type === 'managed-group';
if (!typeIsValid)
if (!typeIsValid) {
throw new InvalidRolePrincipalTypeError(
`Expected a role principal of type 'user', 'group', or 'managed-group'. Got '${type}'.`,
);
}
return this.can.can(`read ${type}`, this.model);
}
}
@@ -76,14 +76,15 @@ export default class GeneratedCredentialLibraryModel extends BaseModel {
path;
@attr('string', {
for: ['vault-generic', 'vault-ldap'],
for: 'vault-generic',
description: 'It indicates the type of credential the library returns.',
})
credential_type;
@attr('object-as-array', {
for: ['vault-generic', 'vault-ldap'],
@attr('object', {
for: 'vault-generic',
description: 'It indicates the credential mapping overrides.',
emptyObjectIfMissing: true,
})
credential_mapping_overrides;
@@ -59,9 +59,9 @@ export default class GeneratedCredentialModel extends BaseModel {
})
username;
// =attributes (username_password, username_password_domain)
// =attributes (username_password, username_password_domain, password)
@attr('string', {
for: ['username_password', 'username_password_domain'],
for: ['username_password', 'username_password_domain', 'password'],
isNestedAttribute: true,
isSecret: true,
description: 'The password for credential.',
+24 -8
View File
@@ -49,7 +49,13 @@ export default class SqliteHandler {
const schema = store.modelFor(type);
const serializer = store.serializerFor(type);
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
let {
page,
pageSize,
select,
query: queryObj,
...remainingQuery
} = query;
let payload,
listToken,
writeToDbPromise,
@@ -101,6 +107,7 @@ export default class SqliteHandler {
payload,
storeToken,
tokenKey,
pushToStore,
serializer,
store,
schema,
@@ -119,7 +126,7 @@ export default class SqliteHandler {
const { sql, parameters } = generateSQLExpressions(type, queryObj, {
page,
pageSize,
select: ['data'],
select: select ?? [{ field: 'data' }],
});
const rows = await this.sqlite.fetchResource({
@@ -129,7 +136,7 @@ export default class SqliteHandler {
const { sql: countSql, parameters: countParams } =
generateSQLExpressions(type, queryObj, {
select: ['count(*) as total'],
select: [{ field: '*', isCount: true, alias: 'total' }],
});
const count = await this.sqlite.fetchResource({
sql: countSql,
@@ -207,6 +214,7 @@ export default class SqliteHandler {
payload,
storeToken,
tokenKey,
pushToStore,
serializer,
store,
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(
store,
schema,
@@ -233,6 +236,19 @@ export default class SqliteHandler {
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
const items = payloadData.map((datum) => {
const params = Object.values(modelMapping[type]).map((key) => {
@@ -8,6 +8,7 @@ import {
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
TYPE_CREDENTIAL_PASSWORD,
} from 'api/models/credential';
/**
* Enum options for credential library.
@@ -19,6 +20,7 @@ export const options = {
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_PASSWORD,
],
mapping_overrides: {
username_password: ['username_attribute', 'password_attribute'],
@@ -32,6 +34,7 @@ export const options = {
'password_attribute',
'domain_attribute',
],
password: ['password_attribute'],
},
};
+4 -1
View File
@@ -12,13 +12,16 @@ import GeneratedCredentialModel from '../generated/models/credential';
export const TYPE_CREDENTIAL_USERNAME_PASSWORD = 'username_password';
export const TYPE_CREDENTIAL_SSH_PRIVATE_KEY = 'ssh_private_key';
export const TYPE_CREDENTIAL_JSON = 'json';
export const TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN = 'username_password_domain';
export const TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN =
'username_password_domain';
export const TYPE_CREDENTIAL_PASSWORD = 'password';
export const TYPES_CREDENTIAL = Object.freeze([
TYPE_CREDENTIAL_USERNAME_PASSWORD,
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_JSON,
TYPE_CREDENTIAL_PASSWORD,
]);
export default class CredentialModel extends GeneratedCredentialModel {
+2 -1
View File
@@ -98,8 +98,9 @@ export default class AccountSerializer extends ApplicationSerializer {
serialized.attributes = {};
}
// New record case
if (password && snapshot?.record?.isNew)
if (password && snapshot?.record?.isNew) {
serialized.attributes.password = password;
}
// Set password custom method
if (snapshot?.adapterOptions?.method === 'set-password') {
serialized = this.serializeForSetPassword(snapshot, password);
@@ -7,7 +7,6 @@ import ApplicationSerializer from './application';
import {
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
options,
} from '../models/credential-library';
@@ -30,8 +29,6 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
return this.serializeVaultGeneric(...arguments);
case TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE:
return this.serializeVaultSshCertificate(...arguments);
case TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP:
return this.serializeVaultLdap(...arguments);
default:
return super.serialize(...arguments);
}
@@ -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.
* @param {Object} serialized - The serialized object.
@@ -80,7 +68,9 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
*/
handleCredentialMappingOverrides(serialized, isNew) {
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
if (credential_mapping_overrides && !isNew) {
serialized.credential_mapping_overrides = options.mapping_overrides[
@@ -28,8 +28,9 @@ export default class CredentialStoreSerializer extends ApplicationSerializer {
if (
!serialized.attributes?.client_certificate_key &&
serialized.attributes?.client_certificate
)
) {
delete serialized.attributes.client_certificate_key;
}
}
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
if (options.isNestedAttribute && json.attributes) {
// 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
json.attributes[key] = null;
}
}
//json.secrets is only present, if there are any updates to the secret fields
+4 -2
View File
@@ -33,16 +33,18 @@ export default class TargetSerializer extends ApplicationSerializer {
const injectedApplicationCredentialSourceIDs =
snapshot?.adapterOptions?.injectedApplicationCredentialSourceIDs;
if (brokeredCredentialSourceIDs)
if (brokeredCredentialSourceIDs) {
serialized = this.serializeWithBrokeredCredentialSources(
snapshot,
brokeredCredentialSourceIDs,
);
if (injectedApplicationCredentialSourceIDs)
}
if (injectedApplicationCredentialSourceIDs) {
serialized = this.serializeWithInjectedApplicationCredentialSources(
snapshot,
injectedApplicationCredentialSourceIDs,
);
}
if (isNew && serialized?.with_aliases) {
// API expects scope id along with every alias value
+2 -1
View File
@@ -18,8 +18,9 @@ export default class UserSerializer extends ApplicationSerializer {
serialize(snapshot) {
let serialized = super.serialize(...arguments);
const accountIDs = snapshot?.adapterOptions?.accountIDs;
if (accountIDs)
if (accountIDs) {
serialized = this.serializewithAccounts(snapshot, accountIDs);
}
return serialized;
}
+44
View File
@@ -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,
});
}
}
+2 -15
View File
@@ -100,6 +100,8 @@ export const modelMapping = {
target_name: 'create_time_values.target.name',
target_scope_id: 'create_time_values.target.scope.id',
target_scope_name: 'create_time_values.target.scope.name',
target_scope_parent_scope_id:
'create_time_values.target.scope.parent_scope_id',
created_time: 'created_time',
},
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 {
// =attributes
+211 -36
View File
@@ -4,9 +4,9 @@
*/
import { modelMapping } from 'api/services/sqlite';
import { searchTables } from 'api/services/sqlite';
import { typeOf } from '@ember/utils';
import { underscore } from '@ember/string';
import { assert } from '@ember/debug';
/**
* Takes a POJO representing a filter query and builds a SQL query.
@@ -31,12 +31,14 @@ export function generateSQLExpressions(
const conditions = [];
const parameters = [];
const tableName = underscore(resource);
const joins = [];
addFilterConditions({ filters, parameters, conditions });
addFilterConditions({ filters, parameters, conditions, joins, tableName });
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 paginationClause = page && pageSize ? `LIMIT ? OFFSET ?` : '';
@@ -44,13 +46,12 @@ export function generateSQLExpressions(
parameters.push(pageSize, (page - 1) * pageSize);
}
const selectClause = `SELECT ${select ? select.join(', ') : '*'} FROM "${tableName}"`;
return {
// Replace any empty newlines or leading whitespace on each line to be consistent with formatting
// This is mainly to help us read and test the generated SQL as it has no effect on the actual SQL execution.
sql: `
${selectClause}
${joinClause}
${whereClause}
${orderByClause}
${paginationClause}`
@@ -60,7 +61,13 @@ export function generateSQLExpressions(
};
}
function addFilterConditions({ filters, parameters, conditions }) {
function addFilterConditions({
filters,
parameters,
conditions,
joins,
tableName,
}) {
if (!filters) {
return;
}
@@ -71,7 +78,7 @@ function addFilterConditions({ filters, parameters, conditions }) {
? filterArrayOrObject
: filterArrayOrObject.values;
if (!filterValueArray || !filterValueArray.length) {
if (!filterValueArray || !filterValueArray.length || key === 'joins') {
continue;
}
@@ -88,8 +95,13 @@ function addFilterConditions({ filters, parameters, conditions }) {
allOperatorsEqual
) {
const operation = firstOperator === 'equals' ? 'in' : 'notIn';
const nullOperator =
firstOperator === 'equals' ? 'IS NULL' : 'IS NOT NULL';
const values = filterValueArray
.filter((f) => f)
.filter(
(filterObjValue) =>
filterObjValue && Object.values(filterObjValue)[0] !== null,
)
.map((filterObjValue) => {
let value = Object.values(filterObjValue)[0];
if (typeOf(value) === 'date') {
@@ -98,7 +110,14 @@ function addFilterConditions({ filters, parameters, conditions }) {
parameters.push(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));
continue;
}
@@ -108,6 +127,16 @@ function addFilterConditions({ filters, parameters, conditions }) {
.map((filterObjValue) => {
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
if (typeOf(value) === 'date') {
value = value.toISOString();
@@ -120,7 +149,7 @@ function addFilterConditions({ filters, parameters, conditions }) {
parameters.push(value);
}
return `${key}${OPERATORS[operation]}`;
return `"${tableName}".${key}${OPERATORS[operation]}`;
});
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({
@@ -142,39 +208,148 @@ function addSearchConditions({
parameters,
conditions,
}) {
if (!search) {
if (!search || !modelMapping[resource]) {
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}"*`);
// Use a subquery to match against the FTS table with rowids as SQLite is
// much more efficient with FTS queries when using rowids or MATCH (or both).
// We could have also used a join here but a subquery is simpler.
conditions.push(
`rowid IN (SELECT rowid FROM ${tableName}_fts WHERE ${tableName}_fts MATCH ?)`,
}
// If there are extra related searches on other tables, add them too
if (search?.relatedSearches?.length > 0) {
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;
}
const fields = Object.keys(modelMapping[resource]);
const searchConditions = parenthetical(
or(
fields.map((field) => {
parameters.push(`%${search}%`);
return `${field}${OPERATORS['contains']}`;
}),
),
);
conditions.push(searchConditions);
// Use a subquery to match against the FTS table with rowids as SQLite is
// much more efficient with FTS queries when using rowids or MATCH (or both).
// We could have also used a join here but a subquery is simpler.
conditions.push(`"${tableName}".${searchSelect} IN (${searchSql})`);
}
function constructOrderByClause(resource, sort) {
const defaultOrderByClause = 'ORDER BY created_time DESC';
function constructSelectClause(select = [{ field: '*' }], tableName) {
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 sortDirection = direction === 'desc' ? 'DESC' : 'ASC';
const attributesWithTableName = attributes?.map(
(attribute) => `"${tableName}".${attribute}`,
);
// We have to check if the attributes are valid for the resource
// 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) {
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.
// Then, a sort on the same condition is performed to ensure upper-case strings are given preference in a tie.
if (isCoalesced) {
return `ORDER BY COALESCE(${attributes.join(', ')}) COLLATE NOCASE ${sortDirection}, COALESCE(${commaSeparatedVals}) ${sortDirection}`;
return `ORDER BY COALESCE(${commaSeparatedVals}) COLLATE NOCASE ${sortDirection}, COALESCE(${commaSeparatedVals}) ${sortDirection}`;
}
const attributesWithNoCollate = attributes
.map((attr) => `${attr} COLLATE NOCASE ${sortDirection}`)
.map((attr) => `"${tableName}".${attr} COLLATE NOCASE ${sortDirection}`)
.join(', ');
const attributesWithDirection = attributes
.map((attr) => `${attr} ${sortDirection}`)
.map((attr) => `"${tableName}".${attr} ${sortDirection}`)
.join(', ');
return `ORDER BY ${attributesWithNoCollate}, ${attributesWithDirection}`;
} else if (modelMapping[resource]?.created_time) {
+1 -1
View File
@@ -28,7 +28,7 @@ import {
// See "Maximum Number Of Host Parameters In A Single SQL Statement" in
// https://www.sqlite.org/limits.html
const MAX_HOST_PARAMETERS = 32766;
const SCHEMA_VERSION = 1;
const SCHEMA_VERSION = 2;
// 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
+19 -17
View File
@@ -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 a contentless FTS table as we will only use the rowids.
-- Note that this only creates the FTS index and cannot reference the target content
-- Create a content FTS table that references the original table
CREATE VIRTUAL TABLE IF NOT EXISTS target_fts USING fts5(
id,
type,
@@ -35,7 +34,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS target_fts USING fts5(
address,
scope_id,
created_time,
content='',
content='target',
);
-- 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
);
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(
id,
@@ -78,7 +78,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS alias_fts USING fts5(
value,
scope_id,
created_time,
content='',
content='alias',
);
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,
scope_id,
created_time,
content='',
content='group',
);
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,
scope_id,
created_time,
content='',
content='role',
);
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,
scope_id,
created_time,
content='',
content='user',
);
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,
scope_id,
created_time,
content='',
content='credential_store',
);
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,
scope_id,
created_time,
content='',
content='scope',
);
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,
scope_id,
created_time,
content='',
content='auth_method',
);
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,
scope_id,
created_time,
content='',
content='host_catalog',
);
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_scope_id TEXT,
target_scope_name TEXT,
target_scope_parent_scope_id TEXT,
created_time 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_scope_id,
target_scope_name,
target_scope_parent_scope_id,
created_time,
content='',
content='session_recording',
);
CREATE TRIGGER IF NOT EXISTS session_recording_ai AFTER INSERT ON session_recording BEGIN
INSERT INTO session_recording_fts(
rowid, id, type, state, start_time, end_time, duration, scope_id, user_id, user_name, target_id, target_name, target_scope_id, target_scope_name, created_time
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 (
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;
CREATE TRIGGER IF NOT EXISTS session_recording_ad AFTER DELETE ON session_recording BEGIN
INSERT INTO session_recording_fts(session_recording_fts, rowid, id, type, state, start_time, end_time, duration, scope_id, user_id, user_name, target_id, target_name, target_scope_id, target_scope_name, created_time)
VALUES('delete', old.rowid, old.id, old.type, old.state, old.start_time, old.end_time, old.duration, old.scope_id, old.user_id, old.user_name, old.target_id, old.target_name, old.target_scope_id, old.target_scope_name, old.created_time);
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.target_scope_parent_scope_id, old.created_time);
END;`;
const createSessionTables = `
@@ -411,7 +413,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS session_fts USING fts5(
user_id,
scope_id,
created_time,
content='',
content='session',
);
CREATE TRIGGER IF NOT EXISTS session_ai AFTER INSERT ON session BEGIN
+6
View File
@@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'api/services/db';
+2 -1
View File
@@ -542,10 +542,11 @@ function routes() {
session.update({
status: pickRandomStatusString(),
});
if (session.id.split('-').pop() % 2)
if (session.id.split('-').pop() % 2) {
session.update({
userId: 'authenticateduser',
});
}
});
}
if (recursive && scope_id === 'global') {
@@ -23,9 +23,6 @@ export default factory.extend({
if (this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC) {
return options.credential_types[i % options.credential_types.length];
}
if (this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP) {
return options.credential_types[2]; // Only supports username, password & domain credential type
}
},
authorized_actions: () =>
permissions.authorizedActionsFor('credential-library') || [
@@ -65,7 +65,7 @@ export default factory.extend({
break;
case 'static':
default:
server.createList('credential', 3, {
server.createList('credential', 5, {
scope,
credentialStore,
});
+8 -4
View File
@@ -12,6 +12,8 @@ import {
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_JSON,
TYPE_CREDENTIAL_PASSWORD,
} from 'api/models/credential';
const types = [...TYPES_CREDENTIAL];
@@ -20,14 +22,16 @@ export default factory.extend({
type: (i) => types[i % types.length],
id() {
switch (this.type) {
case 'ssh_private_key':
case TYPE_CREDENTIAL_SSH_PRIVATE_KEY:
return generatedId('credspk_');
case 'username_password':
case TYPE_CREDENTIAL_USERNAME_PASSWORD:
return generatedId('credup_');
case 'json':
case TYPE_CREDENTIAL_JSON:
return generatedId('credjson_');
case 'username_password_domain':
case TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN:
return generatedId('credupd_');
case TYPE_CREDENTIAL_PASSWORD:
return generatedId('credp_');
}
},
authorized_actions: () =>
+18
View File
@@ -8,6 +8,7 @@ import { trait } from 'miragejs';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
import { faker } from '@faker-js/faker';
import { TYPE_AUTH_METHOD_PASSWORD } from 'api/models/auth-method';
export default factory.extend({
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,
});
}
},
}),
});
+12 -6
View File
@@ -17,7 +17,9 @@ import {
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
TYPE_CREDENTIAL_JSON,
TYPE_CREDENTIAL_PASSWORD,
} from 'api/models/credential';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP } from 'api/models/credential-library';
const randomBoolean = (chance = 0.5) => Math.random() < chance;
const randomFilter = () =>
@@ -111,7 +113,9 @@ export default factory.extend({
server.schema.credentialLibraries,
(cred) =>
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(
server.schema.credentials,
@@ -119,6 +123,7 @@ export default factory.extend({
cred.scopeId === scope.id &&
![
TYPE_CREDENTIAL_JSON,
TYPE_CREDENTIAL_PASSWORD,
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
].includes(cred.type),
);
@@ -127,11 +132,12 @@ export default factory.extend({
const filteredCredentialLibrariesForRDP = selectItems(
server.schema.credentialLibraries,
(cred) =>
cred.scopeId === scope.id &&
[
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
].includes(cred.credential_type),
(cred.scopeId === scope.id &&
[
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
].includes(cred.credential_type)) ||
cred.type == TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
);
const filteredCredentialsForRDP = selectItems(
server.schema.credentials,
+14 -8
View File
@@ -13,7 +13,7 @@ const oidcRequiredAttempts = 3;
const commandHandlers = {
password: {
login: (payload, scopeAttrs) => {
login: (payload, auth_method_id, account_id, scopeAttrs) => {
if (payload.attributes.login_name === 'error') {
return new Response(400);
} else {
@@ -25,9 +25,9 @@ const commandHandlers = {
scope: scopeAttrs,
id: 'token123',
token: 'thetokenstring',
account_id: 'authenticatedaccount',
account_id: account_id || 'authenticatedaccount',
user_id: 'authenticateduser',
auth_method_id: 'authmethod123',
auth_method_id: auth_method_id || 'authmethod123',
created_time: '',
updated_time: '',
last_used_time: '',
@@ -61,7 +61,7 @@ const commandHandlers = {
},
},
),
token: (_, scopeAttrs) => {
token: (_, auth_method_id, account_id, scopeAttrs) => {
oidcAttemptCounter++;
if (oidcAttemptCounter < oidcRequiredAttempts) {
return new Response(202);
@@ -74,9 +74,9 @@ const commandHandlers = {
scope: scopeAttrs,
id: 'token123',
token: 'thetokenstring',
account_id: '1',
account_id: account_id || '1',
user_id: 'authenticateduser',
auth_method_id: 'authmethod123',
auth_method_id: auth_method_id || 'authmethod123',
created_time: '',
updated_time: '',
last_used_time: '',
@@ -90,7 +90,7 @@ const commandHandlers = {
};
// 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(
/(?<id>.[^:]*):(?<method>(.*))/,
);
@@ -100,8 +100,14 @@ export function authHandler({ scopes, authMethods }, request) {
const payload = JSON.parse(request.requestBody);
const { command } = payload;
const scope = scopes.find(authMethod.scopeId);
const account = accounts.where({ authMethodId: authMethod.id })?.models[0];
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
+14
View File
@@ -248,6 +248,20 @@ export default function initializeMockIPC(server, config) {
resumeClientAgent() {}
hasRunningSessions() {}
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',
version: 1,
credential_type: 'ssh_private_key',
credential_mapping_overrides: [
{ key: 'username_attribute', value: 'user' },
],
credential_mapping_overrides: { username_attribute: 'user' },
});
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
@@ -64,9 +62,7 @@ module('Unit | Serializer | credential library', function (hooks) {
http_method: 'GET',
version: 1,
credential_type: 'ssh_private_key',
credential_mapping_overrides: [
{ key: 'private_key_attribute', value: 'test' },
],
credential_mapping_overrides: { private_key_attribute: 'test' },
},
},
});
@@ -291,11 +287,6 @@ module('Unit | Serializer | credential library', function (hooks) {
description: 'Description',
path: '/vault/path',
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 serializedRecord = serializer.serialize(snapshot);
@@ -306,11 +297,6 @@ module('Unit | Serializer | credential library', function (hooks) {
name: 'Name',
description: 'Description',
version: 1,
credential_type: 'username_password_domain',
credential_mapping_overrides: {
username_attribute: 'user',
domain_attribute: 'domain',
},
attributes: {
path: '/vault/path',
},
@@ -326,15 +312,10 @@ module('Unit | Serializer | credential library', function (hooks) {
type: 'credential-library',
attributes: {
type: TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
name: 'Name',
name: 'Test',
description: 'Description',
path: '/vault/path',
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, {
type: TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
credential_store_id: null,
name: 'Name',
name: 'Test',
description: 'Description',
version: 1,
credential_type: 'username_password_domain',
credential_mapping_overrides: {
username_attribute: 'user',
domain_attribute: 'domain',
password_attribute: null,
},
attributes: {
path: '/vault/path',
},
+19
View File
@@ -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 { setupTest } from 'dummy/tests/helpers';
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';
const supportedModels = Object.keys(modelMapping);
const supportedFtsTables = [...searchTables];
module('Unit | Service | sqlite', function (hooks) {
setupTest(hooks);
@@ -38,7 +37,7 @@ module('Unit | Service | sqlite', function (hooks) {
test.each(
'Mapping matches fts table columns',
supportedFtsTables,
supportedModels,
async function (assert, resource) {
const service = this.owner.lookup('service:sqlite');
+378 -62
View File
@@ -20,21 +20,6 @@ module('Unit | Utility | sqlite-query', function (hooks) {
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) {
const query = {
filters: { id: [{ equals: 'tokenKey' }] },
@@ -45,14 +30,14 @@ module('Unit | Utility | sqlite-query', function (hooks) {
sql,
`
SELECT * FROM "token"
WHERE (id = ?)`.removeExtraWhiteSpace(),
WHERE ("token".id = ?)`.removeExtraWhiteSpace(),
);
assert.deepEqual(parameters, ['tokenKey']);
});
test('it executes count queries correctly', function (assert) {
const select = {
select: ['count(*) as total'],
select: [{ field: '*', isCount: true, alias: 'total' }],
};
const { sql, parameters } = generateSQLExpressions('target', {}, select);
@@ -60,11 +45,128 @@ module('Unit | Utility | sqlite-query', function (hooks) {
sql,
`
SELECT count(*) as total FROM "target"
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
ORDER BY "target".created_time DESC`.removeExtraWhiteSpace(),
);
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(
'it generates filters correctly',
{
@@ -74,7 +176,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
type: [{ equals: 'ssh' }],
},
},
expectedWhereClause: 'WHERE (type = ?)',
expectedWhereClause: 'WHERE ("target".type = ?)',
expectedParams: ['ssh'],
},
notEquals: {
@@ -83,7 +185,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
type: [{ notEquals: 'ssh' }],
},
},
expectedWhereClause: 'WHERE (type != ?)',
expectedWhereClause: 'WHERE ("target".type != ?)',
expectedParams: ['ssh'],
},
contains: {
@@ -92,7 +194,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
type: [{ contains: 'ssh' }],
},
},
expectedWhereClause: 'WHERE (type LIKE ?)',
expectedWhereClause: 'WHERE ("target".type LIKE ?)',
expectedParams: ['%ssh%'],
},
greaterThan: {
@@ -102,7 +204,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
numberField: [{ gt: 10 }],
},
},
expectedWhereClause: 'WHERE (created_time > ?) AND (numberField > ?)',
expectedWhereClause:
'WHERE ("target".created_time > ?) AND ("target".numberField > ?)',
expectedParams: [isoDateString, 10],
},
lessThan: {
@@ -112,7 +215,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
numberField: [{ lt: 10 }],
},
},
expectedWhereClause: 'WHERE (created_time < ?) AND (numberField < ?)',
expectedWhereClause:
'WHERE ("target".created_time < ?) AND ("target".numberField < ?)',
expectedParams: [isoDateString, 10],
},
greaterThanOrEqual: {
@@ -122,7 +226,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
numberField: [{ gte: 10 }],
},
},
expectedWhereClause: 'WHERE (created_time >= ?) AND (numberField >= ?)',
expectedWhereClause:
'WHERE ("target".created_time >= ?) AND ("target".numberField >= ?)',
expectedParams: [isoDateString, 10],
},
lessThanOrEqual: {
@@ -132,7 +237,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
numberField: [{ lte: 10 }],
},
},
expectedWhereClause: 'WHERE (created_time <= ?) AND (numberField <= ?)',
expectedWhereClause:
'WHERE ("target".created_time <= ?) AND ("target".numberField <= ?)',
expectedParams: [isoDateString, 10],
},
logicalOperators: {
@@ -150,9 +256,42 @@ module('Unit | Utility | sqlite-query', function (hooks) {
},
},
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'],
},
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 }) {
@@ -162,7 +301,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
`
SELECT * FROM "target"
${expectedWhereClause}
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
ORDER BY "target".created_time DESC`.removeExtraWhiteSpace(),
);
assert.deepEqual(parameters, expectedParams);
},
@@ -184,7 +323,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
sql,
`
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, []);
},
@@ -198,20 +337,20 @@ module('Unit | Utility | sqlite-query', function (hooks) {
attributes: ['name', 'id'],
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: {
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: {
attributes: ['type'],
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 }) {
@@ -247,41 +386,189 @@ module('Unit | Utility | sqlite-query', function (hooks) {
sql,
`
SELECT * FROM "target"
ORDER BY created_time DESC
ORDER BY "target".created_time DESC
LIMIT ? OFFSET ?`.removeExtraWhiteSpace(),
);
assert.deepEqual(parameters, expectedParams);
},
);
test('it generates FTS5 search with filters', function (assert) {
const query = {
search: 'favorite',
filters: {
id: {
logicalOperator: 'and',
values: [{ notEquals: 'id1' }, { notEquals: 'id2' }],
test.each(
'it generates FTS5 search correctly',
{
'string search with filters': {
query: {
search: 'favorite',
},
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);
assert.strictEqual(
sql,
`
SELECT * FROM "target"
WHERE (id NOT IN (?, ?)) AND (status IN (?, ?)) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
);
assert.deepEqual(parameters, [
'id1',
'id2',
'active',
'pending',
'"favorite"*',
]);
});
test.each(
'it generates joins with filters',
{
'basic joins with filters': {
query: {
filters: {
type: [{ equals: 'ssh' }],
joins: [
{
resource: 'session',
query: {
filters: {
status: [{ equals: 'active' }],
},
},
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) {
const query = {
@@ -300,15 +587,15 @@ module('Unit | Utility | sqlite-query', function (hooks) {
const { sql, parameters } = generateSQLExpressions('target', query, {
page: 2,
pageSize: 15,
select: ['data'],
select: [{ field: 'data' }],
});
assert.strictEqual(
sql,
`
SELECT data FROM "target"
WHERE (type = ?) AND (status IN (?, ?)) AND (created_time >= ?) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
ORDER BY name COLLATE NOCASE DESC, name DESC
SELECT "target".data FROM "target"
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 "target".name COLLATE NOCASE DESC, "target".name DESC
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(
'it handles empty and invalid fields',
{
@@ -350,7 +666,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
targetSql,
`
SELECT * FROM "target"
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
ORDER BY "target".created_time DESC`.removeExtraWhiteSpace(),
);
assert.deepEqual(targetParameters, []);
+1
View File
@@ -26,6 +26,7 @@ module.exports = {
browser: true,
},
rules: {
curly: ['error', 'multi-line', 'consistent'],
'ember/no-get': 'off',
'ember/no-get-with-default': 'off',
'ember/no-computed-properties-in-native-classes': 'off',
+1 -2
View File
@@ -101,7 +101,7 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
* @param {string} username
* @return {object}
*/
normalizeData(data, username) {
normalizeData(data) {
// Pull fields up from `data.attributes` for easier access in JavaScript.
// The `attributes` field exists on the Go side for its convenience but is
// unnecessary here.
@@ -109,7 +109,6 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
// Add booleans indicated the scope type
data.isGlobal = data?.scope?.type === 'global';
data.isOrg = data?.scope?.type === 'org';
if (username) data.username = username;
return data;
}
+1 -3
View File
@@ -72,8 +72,6 @@ export default class PasswordAuthenticator extends BaseAuthenticator {
fetch(authEndpointURL, { method: 'post', body }),
);
const json = await response.json();
return response.status < 400
? resolve(this.normalizeData(json, login_name))
: reject();
return response.status < 400 ? resolve(this.normalizeData(json)) : reject();
}
}
+1
View File
@@ -26,6 +26,7 @@ module.exports = {
browser: true,
},
rules: {
curly: ['error', 'multi-line', 'consistent'],
'ember/no-get': 'off',
'ember/no-get-with-default': 'off',
'ember/no-computed-properties-in-native-classes': 'off',
+21 -12
View File
@@ -20,7 +20,7 @@
<DD.Header @hasDivider={{true}}>
<Hds::Form::TextInput::Base
@type='search'
@value={{this.searchTerm}}
@value={{or this.searchTerm @searchTerm}}
placeholder={{t 'actions.narrow-results'}}
aria-label={{t 'actions.narrow-results'}}
{{on 'input' this.filterItems}}
@@ -28,11 +28,18 @@
/>
</DD.Header>
{{/if}}
{{#unless this.itemOptions}}
{{#unless (or this.itemOptions @isLoading)}}
<DD.Description @text={{t 'titles.no-results'}} />
{{/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}}
{{else}}
{{#each this.itemOptions as |itemOption|}}
@@ -47,13 +54,15 @@
{{/each}}
{{/if}}
<DD.Footer @hasDivider={{true}}>
<Hds::Button
@text='Apply'
@isFullWidth={{true}}
@size='small'
{{on 'click' (fn this.applyFilter DD.close)}}
data-test-dropdown-apply-button
/>
</DD.Footer>
{{#unless @isLoading}}
<DD.Footer @hasDivider={{true}}>
<Hds::Button
@text='Apply'
@isFullWidth={{true}}
@size='small'
{{on 'click' (fn this.applyFilter DD.close)}}
data-test-dropdown-apply-button
/>
</DD.Footer>
{{/unless}}
</Hds::Dropdown>
+11 -5
View File
@@ -21,7 +21,8 @@ export default class DropdownComponent extends Component {
*/
get itemOptions() {
let items = this.args.itemOptions;
if (this.searchTerm) {
if (this.searchTerm && !this.args.updateSearchTerm) {
const searchTerm = this.searchTerm.toLowerCase();
items = this.args.itemOptions.filter((item) => {
const isNameMatch = item.name?.toLowerCase().includes(searchTerm);
@@ -29,7 +30,7 @@ export default class DropdownComponent extends Component {
return isNameMatch || isIdMatch;
});
}
return items.slice(0, 500);
return items.slice(0, 250);
}
// =methods
@@ -39,10 +40,15 @@ export default class DropdownComponent extends Component {
* @param {object} event
*/
@action
@debounce(150)
filterItems(event) {
@debounce(250)
async filterItems(event) {
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() {
const { allFilters, selectedFilters } = this.args.filters;
return Object.entries(allFilters).flatMap(([key, value]) => {
assert(`Tags must be an array for key ${key}`, Array.isArray(value));
const paramsSet = new Set(selectedFilters[key]);
const filters = value.filter((item) => paramsSet.has(item.id));
return Object.entries(allFilters).flatMap(([key, values]) => {
assert(`Tags must be an array for key ${key}`, Array.isArray(values));
const uniqueSelectedFilters = [...new Set(selectedFilters[key])];
if (!uniqueSelectedFilters.length) {
return [];
}
return filters.map((item) => ({
id: item.id,
name: item.name,
type: key,
}));
const valueMap = new Map(values.map((value) => [value.id, value.name]));
return uniqueSelectedFilters.filter(Boolean).map((item) => {
const filter = {
id: item,
type: key,
};
if (valueMap.has(item)) {
filter.name = valueMap.get(item);
}
return filter;
});
});
}
@@ -4,9 +4,8 @@
}}
{{#if @model.type}}
{{component
(concat 'form/authenticate/' @model.type)
onSubmit=@submit
disabled=@disabled
}}
<this.authenticateFormComponent
@onSubmit={{@submit}}
@disabled={{@disabled}}
/>
{{/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}`;
// 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 = {};
}
// If the route has no resource filters list specified yet, create one.
if (!routeClass.resourceFilterParams) routeClass.resourceFilterParams = [];
+34
View File
@@ -3,6 +3,9 @@
* SPDX-License-Identifier: BUSL-1.1
*/
@use 'rose/app/styles/rose/variables/sizing' as sizing;
@use 'sass:meta';
.filters-applied {
display: flex;
flex-wrap: wrap;
@@ -72,3 +75,34 @@
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);
});
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(
'itemOptions',
@@ -145,7 +145,7 @@ module('Integration | Component | dropdown/index', function (hooks) {
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
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');
});
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
edit-worker-filter: Edit Worker Filter
add-worker-filter: Add Worker Filter
open: Open
+16
View File
@@ -49,6 +49,7 @@ titles:
project-actions: Project Actions
user-menu: User Menu
back-link: 'Back to {scope}'
authenticated: Signed in as
descriptions:
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
@@ -141,6 +142,21 @@ settings:
alerts:
cache-daemon: There may be a problem with the cache daemon
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:
title: Filter generator
description: Choose what you want to format into a filter.
@@ -913,11 +913,12 @@ credential-library:
ssh_private_key: SSH Private Key
username_password: Username & Password
username_password_domain: Username, Password & Domain
username_attribute: Username
password_attribute: Password
private_key_passphrase_attribute: Private Key Passphrase
private_key_attribute: Private Key
domain_attribute: Domain
password: Password
username_attribute: username_attribute
password_attribute: password_attribute
private_key_passphrase_attribute: private_key_passphrase_attribute
private_key_attribute: private_key_attribute
domain_attribute: domain_attribute
actions:
create: New Credential Library
delete: Delete Credential Library
@@ -989,12 +990,14 @@ credential:
ssh_private_key: Username & Key Pair
username_password_domain: Username, Password & Domain
json: JSON
password: Password
unknown: Unknown
help:
username_password: Connect using the provided username and password.
ssh_private_key: Connect using a username, public key, and private key.
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:
username:
label: Username
+1
View File
@@ -26,6 +26,7 @@ module.exports = {
browser: true,
},
rules: {
curly: ['error', 'multi-line', 'consistent'],
'ember/no-jquery': 'error',
'ember/no-new-mixins': 0,
'ember/no-get': 'off',
+6 -6
View File
@@ -45,13 +45,13 @@ form {
.hds-table__th-sort button {
color: var(--token-color-foreground-strong);
}
}
// 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
// as they already forced it with !important and we can't override it otherwise
.sr-only {
position: unset !important;
}
// 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
// as they already forced it with !important and we can't override it otherwise
.sr-only {
position: unset !important;
}
.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) {
.ember-application:not(.rose-theme-light) {
@include dark-mode;
@include dark-mode-modal-override;
@include dark-mode-ember-dropdown-override;
}
}
.ember-application.rose-theme-dark {
@include dark-mode;
@include dark-mode-modal-override;
@include dark-mode-ember-dropdown-override;
}
+1 -1
View File
@@ -14,7 +14,7 @@ html {
body {
// Used by Rose::Tabs and '/' in breadcrumbs
// and link list panel
font-family: typography.$family-primary;
font-family: var(--token-typography-font-stack-text);
// this is used across the app
line-height: typography.line-height(m);
@@ -15,7 +15,7 @@
max-width: 100%;
display: flex;
flex: 1;
font-family: ui-monospace, Menlo, monospace;
font-family: var(--token-typography-font-stack-code);
}
.rose-autosize {
@@ -28,9 +28,10 @@
.rose-form-checkbox-label {
// label styles
@include type.type(s, semibold);
@include type.type(s);
color: currentcolor;
font-weight: var(--token-typography-font-weight-semibold);
&,
.rose-form-checkbox-label-description {
@@ -53,7 +54,7 @@
pointer-events: none;
content: ' ';
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
border-radius: sizing.rems(xxxs);
border-radius: var(--token-form-checkbox-border-radius);
}
&::after {
@@ -10,7 +10,9 @@
margin-bottom: sizing.rems(m);
.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 {
@@ -21,7 +21,7 @@
margin-bottom: 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-radius: sizing.rems(xxxs);
border-radius: var(--token-border-radius-x-small);
box-shadow: 0 sizing.rems(xxs) sizing.rems(xxs)
rgba(var(--stark-components), var(--opacity-2));
background: var(--ui-gray-subtler-5)
@@ -20,8 +20,9 @@
}
.rose-header-brand-text {
@include type.type(m, normal);
@include type.type(m);
font-weight: var(--token-typography-font-weight-regular);
margin: 0;
margin-left: sizing.rems(s);
white-space: nowrap;
@@ -43,7 +43,7 @@
// First cell (key) is approximately 25%
&:nth-child(1) {
width: 25%;
font-weight: typography.font-weight(semibold);
font-weight: var(--token-typography-font-weight-semibold);
}
// Second cell (value) is approximately 75%
@@ -22,10 +22,11 @@
// TODO revisit the title
.rose-nav-title {
@include type.type(xs, semibold);
@include type.type(xs);
display: none;
color: #{sass.color(ui-gray, 400)};
font-weight: var(--token-typography-font-weight-semibold);
text-transform: uppercase;
$line-height: math.div(20, 12);
@@ -17,8 +17,9 @@
--background: 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;
color: var(--color);
background-color: var(--background);
@@ -9,10 +9,7 @@
* Mixin for specifying a font-size/line-height size on an element, with
* an option for specifying weight.
*/
@mixin type($size, $weight: &) {
@mixin type($size) {
font-size: typography.font-size($size);
line-height: typography.line-height($size);
@if $weight {
font-weight: typography.font-weight($weight);
}
}
@@ -11,29 +11,6 @@
* 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
$base-font-size: 16;
@@ -84,10 +61,3 @@ $line-heights: (
@function line-height($name) {
@return map.get($line-heights, $name);
}
/**
* Returns a named font weight.
*/
@function font-weight($name) {
@return map.get($font-weights, $name);
}
+1 -1
View File
@@ -30,7 +30,7 @@
},
"dependencies": {
"@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/flight-icons": "^3.10.0",
"@nullvoxpopuli/ember-composable-helpers": "^5.2.10",
+2 -2
View File
@@ -16,7 +16,7 @@
width: 9rem;
// 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;
box-shadow: 0 3px 6px rgba(var(--stark-components), var(--opacity-3));
list-style-type: none;
@@ -36,7 +36,7 @@
.swatch-chip {
height: 5rem;
border: 0.5rem solid var(--subtle);
border-radius: 4px;
border-radius: var(--token-border-radius-small);
}
.swatch-detail {
+1
View File
@@ -20,6 +20,7 @@ module.exports = {
es2021: true,
},
rules: {
curly: ['error', 'multi-line', 'consistent'],
'no-empty-pattern': ['error', { allowObjectPatternsAsParameters: true }],
},
overrides: [
+13 -17
View File
@@ -147,11 +147,11 @@ export class CredentialStoresPage extends BaseResourcePage {
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.getByRole('combobox', { name: 'Type' }).click();
await this.page
.getByRole('group', { name: 'Type' })
.getByLabel('Username & Key Pair')
.getByRole('option', { name: 'Username & Key Pair' })
.click();
await this.page
.getByLabel('Username Required', { exact: true })
@@ -207,11 +207,11 @@ export class CredentialStoresPage extends BaseResourcePage {
}
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.getByRole('combobox', { name: 'Type' }).click();
await this.page
.getByRole('group', { name: 'Type' })
.getByLabel('Username & Password')
.getByRole('option', { name: 'Username & Password' })
.click();
await this.page
.getByLabel('Username Required', { exact: true })
@@ -271,11 +271,11 @@ export class CredentialStoresPage extends BaseResourcePage {
}
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.getByRole('combobox', { name: 'Type' }).click();
await this.page
.getByRole('group', { name: 'Type' })
.getByLabel('Username, Password & Domain')
.getByRole('option', { name: 'Username, Password & Domain' })
.click();
await this.page
.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: 'New', exact: true }).click();
await this.page
.getByLabel('Name (Optional)', { exact: true })
.getByLabel('Name', { exact: true })
.fill(credentialLibraryName);
await this.page
.getByLabel('Description (Optional)')
.fill('This is an automated test');
await this.page.getByLabel('Description').fill('This is an automated test');
await this.page
.getByRole('group', { name: 'Type' })
.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
.getByLabel('Name (Optional)', { exact: true })
.getByLabel('Name', { exact: true })
.fill(credentialLibraryName);
await this.page
.getByLabel('Description (Optional)')
.fill('This is an automated test');
await this.page.getByLabel('Description').fill('This is an automated test');
await this.page
.getByRole('group', { name: 'Type' })
.getByLabel('SSH Certificates')
+1 -2
View File
@@ -19,8 +19,7 @@ export class LoginPage extends BaseResourcePage {
await this.page.getByRole('button', { name: 'Sign In' }).click();
await expect(
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();
await expect(this.page.getByText(loginName)).toBeEnabled();
+8 -9
View File
@@ -60,16 +60,15 @@ export class OrgsPage extends BaseResourcePage {
async chooseScopeFromDropdown(fromScope, toScope) {
await this.page
.getByRole('button', { name: fromScope, exact: true })
.click()
await this.page
.getByRole('option', { name: toScope, exact: true })
.click()
.click();
await this.page.getByRole('option', { name: toScope, exact: true }).click();
await expect(
this.page.getByRole('button', { name: toScope, exact: true })
).toBeVisible()
if (toScope != 'Global')
this.page.getByRole('button', { name: toScope, exact: true }),
).toBeVisible();
if (toScope !== 'Global') {
await expect(
this.page.getByRole('link', { name: toScope, exact: true })
).toBeVisible()
this.page.getByRole('link', { name: toScope, exact: true }),
).toBeVisible();
}
}
}
+22 -315
View File
@@ -9,73 +9,15 @@ import { nanoid } from 'nanoid';
import { BaseResourcePage } from './base-resource.js';
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.
* @param {string} targetType Type of target (ssh, rdp, tcp)
* @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 createTargetWithAddressAndAlias(
address,
port,
alias,
targetType = 'tcp',
) {
async createTarget({ targetType, port, address, alias }) {
const targetName = 'Target ' + nanoid();
let targetTypeLabel;
@@ -92,9 +34,6 @@ export class TargetsPage extends BaseResourcePage {
targetTypeLabel = 'Generic TCP';
break;
}
default: {
throw new Error(`Unexpected target type passed ${targetType}`);
}
}
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.getByLabel('Name').fill(targetName);
await this.page.getByLabel('Description').fill('This is an automated test');
await this.page.getByLabel(targetTypeLabel).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 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();
if (targetType) {
if (targetTypeLabel) {
await this.page
.getByRole('group', { name: 'Type' })
.getByLabel(targetTypeLabel)
.click();
} else {
throw new Error(`Unsupported target type: ${targetType}`);
}
}
if (address) await this.page.getByLabel('Target Address').fill(address);
if (port) await this.page.getByLabel('Default Port').fill(port);
if (alias) {
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();
+2 -2
View File
@@ -40,7 +40,7 @@ export class WorkersPage extends BaseResourcePage {
await this.page
.getByRole('row')
.filter({ has: this.page.getByRole('cell', { name: origKey }) })
.getByRole('cell', { name: 'Overflow Options' })
.getByRole('button', { name: 'Overflow Options' })
.click();
await this.page.getByRole('button', { name: 'Edit Tag' }).click();
@@ -59,7 +59,7 @@ export class WorkersPage extends BaseResourcePage {
await this.page
.getByRole('row')
.filter({ has: this.page.getByRole('cell', { name: key }) })
.getByRole('cell', { name: 'Overflow Options' })
.getByRole('button', { name: 'Overflow Options' })
.click();
await this.page.getByRole('button', { name: 'Remove Tag' }).click();
+15 -13
View File
@@ -46,10 +46,11 @@ test.describe('Aliases (Enterprise)', () => {
await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createSshTargetWithAddressEnt(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
address: targetAddress,
});
const credentialStoresPage = new CredentialStoresPage(page);
await credentialStoresPage.createStaticCredentialStore();
const credentialName =
@@ -165,12 +166,12 @@ test.describe('Aliases (Enterprise)', () => {
await projectsPage.createProject();
alias = 'example.alias.' + nanoid();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createTargetWithAddressAndAlias(
targetAddress,
targetPort,
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
address: targetAddress,
alias,
'ssh',
);
});
const credentialStoresPage = new CredentialStoresPage(page);
await credentialStoresPage.createStaticCredentialStore();
const credentialName =
@@ -236,10 +237,11 @@ test.describe('Aliases (Enterprise)', () => {
const projectsPage = new ProjectsPage(page);
const projectName = await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createSshTargetWithAddressEnt(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
address: targetAddress,
});
const credentialStoresPage = new CredentialStoresPage(page);
await credentialStoresPage.createStaticCredentialStore();
const credentialName =
+12 -12
View File
@@ -44,10 +44,10 @@ test.describe('Aliases', () => {
const projectsPage = new ProjectsPage(page);
await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createTargetWithAddress(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
port: targetPort,
address: targetAddress,
});
// Create alias for target
const aliasName = 'Alias ' + nanoid();
@@ -148,11 +148,11 @@ test.describe('Aliases', () => {
await projectsPage.createProject();
alias = 'example.alias.' + nanoid();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createTargetWithAddressAndAlias(
targetAddress,
targetPort,
const targetName = await targetsPage.createTarget({
port: targetPort,
address: targetAddress,
alias,
);
});
// Connect to target using alias
connect = await boundaryCli.connectToAlias(alias, sshUser, sshKeyPath);
@@ -206,10 +206,10 @@ test.describe('Aliases', () => {
const projectsPage = new ProjectsPage(page);
const projectName = await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createTargetWithAddress(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
port: targetPort,
address: targetAddress,
});
// Create new alias from scope page
await boundaryCli.authenticateBoundary(
@@ -55,7 +55,10 @@ test(
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Auth Methods' })
.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();
const ldapAuthMethodName = 'LDAP ' + nanoid();
@@ -133,9 +133,7 @@ test(
).toBeVisible();
// Log back in as an admin
await page.getByRole('button', { name: 'User Menu' }).click();
await page.getByRole('button', { name: 'Sign Out' }).click();
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible();
await loginPage.logout(email);
await loginPage.login(adminLoginName, adminPassword);
await expect(
page.getByRole('navigation', { name: 'breadcrumbs' }).getByText('Orgs'),
@@ -41,10 +41,11 @@ test(
const projectsPage = new ProjectsPage(page);
const projectName = await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createSshTargetWithAddressEnt(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
address: targetAddress,
});
const credentialStoresPage = new CredentialStoresPage(page);
await credentialStoresPage.createStaticCredentialStore();
const credentialName =
@@ -31,10 +31,10 @@ test.beforeEach(async ({ page, targetAddress, targetPort }) => {
const projectsPage = new ProjectsPage(page);
projectName = await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
targetName = await targetsPage.createTargetWithAddress(
targetAddress,
targetPort,
);
targetName = await targetsPage.createTarget({
port: targetPort,
address: targetAddress,
});
const credentialStoresPage = new CredentialStoresPage(page);
await credentialStoresPage.createStaticCredentialStore();
});
@@ -191,12 +191,10 @@ test(
.getByRole('link', { name: 'Credentials', 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
.getByRole('group', { name: 'Type' })
.getByLabel('JSON')
.click();
await page.getByRole('combobox', { name: 'Type' }).click();
await page.getByRole('option', { name: 'JSON' }).click();
await page.getByText('{}').click();
const testName = 'name-json';
const testPassword = 'password-json';
@@ -68,10 +68,11 @@ test(
const projectsPage = new ProjectsPage(page);
const projectName = await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createSshTargetWithAddressEnt(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
address: targetAddress,
});
const credentialStoresPage = new CredentialStoresPage(page);
await credentialStoresPage.createVaultCredentialStoreWithWorkerFilter(
vaultAddrPrivate,
@@ -85,10 +85,10 @@ test(
const projectsPage = new ProjectsPage(page);
const projectName = await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createTargetWithAddress(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
port: targetPort,
address: targetAddress,
});
const credentialStoresPage = new CredentialStoresPage(page);
await credentialStoresPage.createVaultCredentialStore(
vaultAddr,
@@ -99,11 +99,9 @@ test(
await page.getByRole('link', { name: 'Credential Libraries' }).click();
await page.getByRole('link', { name: 'New', exact: true }).click();
await page
.getByLabel('Name (Optional)', { exact: true })
.getByLabel('Name', { exact: true })
.fill(credentialLibraryName);
await page
.getByLabel('Description (Optional)')
.fill('This is an automated test');
await page.getByLabel('Description').fill('This is an automated test');
await page
.getByLabel('Vault Path')
.fill(`${secretsPath}/data/${secretName}`);
@@ -156,7 +156,10 @@ test.describe('AWS', () => {
// Create a target and add DHC host set as a host source
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createSshTargetEnt(targetPort);
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
});
await targetsPage.addHostSourceToTarget(hostSetName);
// Add another host source
@@ -156,7 +156,9 @@ test.describe('AWS', () => {
// Create a target and add DHC host set as a host source
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createTarget(targetPort);
const targetName = await targetsPage.createTarget({
port: targetPort,
});
await targetsPage.addHostSourceToTarget(hostSetName);
// Add another host source
@@ -93,10 +93,11 @@ test(
await page.getByRole('link', { name: orgName }).click();
await page.getByRole('link', { name: projectName }).click();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createSshTargetWithAddressEnt(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
address: targetAddress,
});
await targetsPage.addEgressWorkerFilterToTarget(
`"${workerTagEgress}" in "/tags/type"`,
);
@@ -97,10 +97,11 @@ test(
await page.getByRole('link', { name: orgName }).click();
await page.getByRole('link', { name: projectName }).click();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createSshTargetWithAddressEnt(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
address: targetAddress,
});
await targetsPage.addIngressWorkerFilterToTarget(
`"${workerTagEgress}" in "/tags/type"`,
);
@@ -99,10 +99,11 @@ test(
// Create target
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createSshTargetWithAddressEnt(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
address: targetAddress,
});
// Create credentials
const credentialStoresPage = new CredentialStoresPage(page);
@@ -89,10 +89,11 @@ test(
// Create target
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createSshTargetWithAddressEnt(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
address: targetAddress,
});
// Create credentials
const credentialStoresPage = new CredentialStoresPage(page);
+19 -13
View File
@@ -41,10 +41,11 @@ test(
const projectsPage = new ProjectsPage(page);
const projectName = await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createTcpTargetWithAddressEnt(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
targetType: 'tcp',
port: targetPort,
address: targetAddress,
});
await boundaryCli.authenticateBoundary(
controllerAddr,
@@ -108,10 +109,11 @@ test(
const projectsPage = new ProjectsPage(page);
const projectName = await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createSshTargetWithAddressEnt(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
address: targetAddress,
});
const credentialStoresPage = new CredentialStoresPage(page);
await credentialStoresPage.createStaticCredentialStore();
const credentialName =
@@ -194,7 +196,10 @@ test(
// Create target
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createSshTargetEnt(targetPort);
const targetName = await targetsPage.createTarget({
targetType: 'ssh',
port: targetPort,
});
await targetsPage.addHostSourceToTarget(hostSetName);
// Add/Remove another host source
@@ -285,10 +290,11 @@ test(
);
const targetsPage = new TargetsPage(page);
const rdpTarget = await targetsPage.createRDPTargetWithAddressEnt(
targetAddress,
targetPort,
);
const rdpTarget = await targetsPage.createTarget({
targetType: 'rdp',
port: targetPort,
address: targetAddress,
});
await targetsPage.addBrokeredCredentialsToTarget(
rdpTarget,
credentialName,
+12 -7
View File
@@ -57,7 +57,7 @@ test(
// Create target
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createTarget(targetPort);
const targetName = await targetsPage.createTarget({ port: targetPort });
await targetsPage.addHostSourceToTarget(hostSetName);
// Add/Remove another host source
@@ -127,10 +127,10 @@ test(
const projectsPage = new ProjectsPage(page);
const projectName = await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
const targetName = await targetsPage.createTargetWithAddress(
targetAddress,
targetPort,
);
const targetName = await targetsPage.createTarget({
port: targetPort,
address: targetAddress,
});
await boundaryCli.authenticateBoundary(
controllerAddr,
@@ -191,7 +191,10 @@ test(
const projectsPage = new ProjectsPage(page);
await projectsPage.createProject();
const targetsPage = new TargetsPage(page);
await targetsPage.createTargetWithAddress(targetAddress, targetPort);
await targetsPage.createTarget({
port: targetPort,
address: targetAddress,
});
// Update target
await page.getByRole('button', { name: 'Edit Form' }).click();
@@ -234,7 +237,9 @@ test(
.locator('.CodeMirror')
.getByRole('textbox')
.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
.locator('.CodeMirror')
+39 -1
View File
@@ -6,6 +6,12 @@
import { expect, test } from '../fixtures/baseTest.js';
import * as boundaryHttp from '../../helpers/boundary-http.js';
import { textToMatch } from '../fixtures/tesseractTest.js';
import {
isRdpClientInstalled,
isRdpRunning,
killRdpProcesses,
isOSForRdpSupported,
} from '../../helpers/rdp.js';
const hostName = 'Host name for test';
let org;
@@ -117,7 +123,6 @@ test.beforeEach(
});
// 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, {
scopeId: project.id,
type: 'rdp',
@@ -285,4 +290,37 @@ test.describe('Targets tests', () => {
authedPage.getByRole('link', { name: targetWithHost.name }),
).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);
});
});
+60
View File
@@ -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
}
}
+2 -2
View File
@@ -30,8 +30,8 @@
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/eslint-parser": "^7.21.3",
"@openapitools/openapi-generator-cli": "^2.23.4",
"@playwright/test": "^1.49.0",
"@openapitools/openapi-generator-cli": "^2.25.2",
"@playwright/test": "^1.56.1",
"concurrently": "^9.1.0",
"dotenv": "^16.4.5",
"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