Compare commits

...
86 Commits
Author SHA1 Message Date
Carlos Alcaide Corvo 2d1e15a7bb Update changelog and cli version (#2280)
* Update changelog and cli version
2024-04-30 10:21:25 -07:00
Lisbet Alvarez dd92321b81 fix: 🐛 update label & description (#2279) 2024-04-29 11:52:45 -07:00
Lisbet Alvarez dce45031cb fix: 🐛 recording storage path not being set (#2276) 2024-04-26 15:49:29 -07:00
Zhihe Li 2666ec8244 fix: 🐛 Fix account password not being properly set (#2275) 2024-04-26 13:40:30 -04:00
Michael Li 3674d07762 test(e2e): Address flake (#2274)
Observed a flake when the Recording ID also contained an `OK`, causing a conflict with finding this OK button.
2024-04-25 13:52:26 -04:00
Zhihe Li dccf06cb0c fix: 🐛 Fix error when deleting auth method (#2273)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13624
2024-04-25 11:48:55 -04:00
Carlos Alcaide CorvoandZhihe Li c64e8a2993 Fix storage bucket empty state message (#2272)
* Fix text

* Update addons/core/translations/resources/en-us.yaml

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2024-04-24 15:08:04 -07:00
Lisbet Alvarez 5ae490c28c fix: 🐛 small updates to cred-library, cred-store & host-catalog (#2271) 2024-04-24 11:16:06 -07:00
Dharini Jeeva bedcdee048 fix: 🐛 permissions (#2269)
* fix: 🐛 permissions
2024-04-24 11:04:11 -07:00
Michael Li bd394d9517 fix: Update LDAP address example (#2270) 2024-04-24 12:22:04 -04:00
Lisbet Alvarez a6bdc4fa24 fix: 🐛 action errors on auth-method refresh (#2268) 2024-04-23 16:37:01 -07:00
Zhihe Li 3e9a06a5b5 fix: 🐛 Fix adding host set error in targets (#2267) 2024-04-23 10:49:33 -04:00
Zhihe Li 19908768a4 fix: 🐛 Fix HDS labels and helpers in host sets (#2266)
* fix: 🐛 Fix HDS labels and helpers in host sets
2024-04-22 20:56:26 -04:00
Dharini Jeeva 4274a045f7 fix: 🐛 add details (#2265) 2024-04-22 13:00:00 -07:00
Lisbet Alvarez 1270458430 fix: 🐛 add missing pagination component to cred stores (#2264)
* fix: 🐛 add missing pagination component to cred stores

* test: 💍 fix cred store acceptance tests
2024-04-22 12:43:39 -07:00
Dharini Jeeva 4c342061d3 fix: 🐛 aliasing bugs (#2257)
* fix: 🐛 aliasing bugs
2024-04-22 12:16:00 -07:00
Dharini Jeeva 5ce83d9239 fix: 🐛 title (#2263) 2024-04-22 10:57:40 -07:00
Lisbet Alvarez 148f2db941 refactor: 💡 remove route-action usage for auth-methods (#2178)
* refactor: 💡 remove route-action usage for auth-methods

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12603
2024-04-22 10:33:06 -07:00
hashicorp-copywrite[bot] f65f483611 [COMPLIANCE] Add Copyright and License Headers (#2262)
Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2024-04-22 11:47:53 -05:00
Lisbet Alvarez cc9003a118 fix: 🐛 required & optional labels in oidc auth-methods form (#2261) 2024-04-22 09:05:29 -07:00
Michael Li 20f8c99013 test(e2e): Fixes for 0.16.0 (#2258) 2024-04-22 10:39:53 -04:00
Dharini Jeeva 536ebc1c0f chore: 🤖 add doc links for aliases (#2260) 2024-04-19 13:37:51 -07:00
Zhihe Li 79a932bebb feat: 🎸 Add aliases to list view (#2256)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13487
2024-04-19 16:18:32 -04:00
Carlos Alcaide Corvo f246c24a08 Add field validation for scope within Storage bucket creation (#2259) 2024-04-19 11:42:24 -07:00
Michael Li d31cfca138 chore(e2e): Update playwright to 1.43 (#2254)
When running the Admin UI e2e tests against 0.16.0, there were some issues with trying to click on a link on the new pagination tables (links were not seen as visible). For some reason, updating the playwright version fixed this.
2024-04-18 11:49:46 -04:00
Carlos Alcaide Corvo 298e804500 Fix an afterModel check to avoid reloading the page when the queryParam gets updated (#2255) 2024-04-18 08:03:13 -07:00
Dharini Jeeva f9cd1c331f feat: 🎸 show associated aliases in target detail page (#2249)
* feat: 🎸 show associated aliases in target detail page
2024-04-17 12:39:34 -07:00
Dharini Jeeva a0fa66a14d fix: 🐛 plural title (#2252) 2024-04-17 12:26:58 -07:00
Dharini Jeeva 086d9da028 fix: 🐛 place modal outside table (#2251) 2024-04-17 11:40:23 -07:00
Cameron Perera 9b20b150eb fix: 🐛 transition aborted error when deleting group (#2250) 2024-04-17 11:13:58 -05:00
Zhihe Li 6bbddc56b1 fix: 🐛 Fix windows being unable to search client daemon (#2245)
When using the client daemon to search on windows, we use
the CLI instead of the unix socket for our requests.
Unfortunately that means we spawn a separate process to
retrive the results and one idiosyncracy of this is that
`spawnSync` buffers its outputs which means we were hitting
the default max buffer size. `spawn` doesn't have this
issue because it streams its outputs and we didn't previously
use `spawnSync` for any command that produced large amounts
of output.

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13466
2024-04-17 07:40:09 -07:00
Carlos Alcaide Corvo 0870a8f623 MinIO support for Admin UI (#2248)
Add support for MinIO creation form and list
2024-04-16 14:17:23 -07:00
Cameron Perera 98c16ad422 fix: 🐛 add missing scope_id from credential sources for targets (#2247) 2024-04-16 11:31:54 -05:00
Zhihe LiandCameron Perera 9156c8612e fix: 🐛 Add back scope_id as part of request query parameter (#2246)
Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
2024-04-16 11:15:16 -05:00
Lisbet Alvarez a43756895d fix: 🐛 auth-methods deleted items are showing on table view (#2244)
* fix: 🐛 auth-methods deleted items are showing on table view
2024-04-15 16:30:43 -07:00
Lisbet Alvarez adc50a939f refactor: 💡 remove uneeded rose cards component (#2243)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13321
2024-04-15 10:37:26 -07:00
hashicorp-copywrite[bot] 37cbb74c76 [COMPLIANCE] Add Copyright and License Headers (#2242)
Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2024-04-15 11:51:03 -05:00
Dharini Jeeva 80b633a063 Alias clear modal (#2239)
* fix: 🐛 permissions to create aliases within targets
2024-04-15 09:15:38 -07:00
Dharini Jeeva bd65c91ff9 chore: 🤖 alias target route (#2228) 2024-04-12 16:36:17 -07:00
Lisbet Alvarez c550faddb4 Icu 13218 update indexed db parser unit tests to test for filtering on boolean fields (#2238)
* test: 💍 unit tests for bool fields
* chore: 🤖 add deprecation handling

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13218
2024-04-12 16:22:27 -07:00
Cameron Perera 5e7c25a1fa refactor: 💡 update groups and workers "new" button to HDS (#2236) 2024-04-12 14:55:34 -04:00
ded8bdadd1 Icu 12592 add filtering pagination support for auth methods (#2209)
* feat: 🎸 add filtering & pagination to auth methods

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12592

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
2024-04-12 14:55:34 -04:00
Cameron Perera 13da35c06b Icu 13357 create loading screen for admin UI list routes (#2231)
* test: 💍 fix failing tests

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13357

* feat: 🎸 add generic loading indicator for admin ui

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13357

* feat: 🎸 add custom loading logic for search and filtering

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13357

* chore: 🤖 fix lint error

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13357

* refactor: 💡 remove commented helper for sessions list

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13357

* style: 💄 update loading indicator margin to relative unit

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13357

* refactor: 💡 remove unneeded custom loading action

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13357
2024-04-12 14:55:34 -04:00
Lisbet Alvarez d52957d87b fix: 🐛 breadcrumbs on scopes route not showing since model move (#2235) 2024-04-12 14:55:34 -04:00
Zhihe Liandcameronperera 33d359e692 Fix tests from rebase (#2233)
* fix: 🐛 Fix tests broken by rebase onto main

---------

Co-authored-by: cameronperera <cameron.perera@hashicorp.com>
2024-04-12 14:55:34 -04:00
Cameron Perera 626bdc2c92 Icu 13374 add permission checks for filtering resources in sessions and targets (#2226)
* refactor: 💡 hide active sessions filter if no list permissions

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374

* test: 💍 add active sessions filter permissions test

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374

* refactor: 💡 add permissions check for targets and users

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374

* refactor: 💡 address PR feedback

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374

* refactor: 💡 move user permission check to a helper func

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374

* refactor: 💡 move permissions logic to user and target helper

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374
2024-04-12 14:55:34 -04:00
Lisbet Alvarez edba33bbda Icu 13359 refactor query requests for resources added to indexed db (#2224)
* refactor: 💡 update query requests for resources in indexedDB

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13359
2024-04-12 14:55:34 -04:00
Zhihe Li 5d32d987bc feat: 🎸 Update how we store tokens in indexed-db (#2223)
Added logic to make the list tokens stored in indexed DB more granular.
I get a hash of the query parameters that will get sent to the controller
and use that as the id for the token. This also means extra logic for when
a token is invalid as we will need to delete all tokens of the same
resource type.
2024-04-12 14:55:34 -04:00
Lisbet Alvarez 6c59baa413 refactor: 💡 reduce network requests & call indexedDB directly (#2217)
* refactor: 💡 reduce network requests & call indexedDB directly

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13251
2024-04-12 14:55:34 -04:00
Cameron Perera c2c0b38187 fix: 🐛 move query inside list permission check (#2222)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13356
2024-04-12 14:55:34 -04:00
Lisbet Alvarez 1d7892230c Icu 13302 add filtering pagination support for scopes (#2216)
* feat: 🎸 add filtering and pagination to scopes

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13302
2024-04-12 14:55:34 -04:00
Cameron Perera ece9c9b0c8 refactor: 💡 add include_terminated param to sessions query (#2219)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13130
2024-04-12 14:55:34 -04:00
Lisbet Alvarez 9a43c74e60 refactor: 💡 add active sessions column to targets table (#2196)
* refactor: 💡 add active sessions column to targets table

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13085

* test: 💍 add test case

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13085
2024-04-12 14:55:33 -04:00
Cameron Perera eb4ec5f6de refactor: 💡 update Dropdown component to have name & toggleText (#2218)
* refactor: 💡 update Dropdown component to have name & toggleText

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13336

* test: 💍 remove hds class selector

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13336
2024-04-12 14:55:33 -04:00
Zhihe Li a39b208b48 Icu 12781 add filtering pagination support for host catalogs (#2213)
* feat: 🎸 Add searching for host catalogs

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12781
2024-04-12 14:55:33 -04:00
Lisbet AlvarezandZhihe Li 22353e7df6 Icu 12782 add filtering pagination support for credential stores (#2212)
* feat: 🎸 add filtering & pagination support for credential-store

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12782

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2024-04-12 14:55:33 -04:00
Cameron Perera 713ffd8d1b fix: 🐛 add parentScope.id to refresh action for sessions (#2215)
* fix: 🐛 add parentScope.id to refresh action for sessions

* refactor: 💡 remove scope_id from users and targets query
2024-04-12 14:55:33 -04:00
Cameron Perera 6e75439652 Icu 12587 add filtering pagination support for sessions (#2194)
* feat: 🎸 add pagination and filtering to sessions page

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 fix handling of null values in indexed-db-query

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 add method signatures where missing

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 address PR feedback

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* fix: 🐛 add override for table header color when sortable

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 remove poller and add manual refresh action

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 remove force_refresh from sessions route queries

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 add user getter to session model

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 remove logicalOperator value from queries

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 move action to controller from router

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 remove cleanValues from indexed-db-query

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 remove potential null values for user/target ids

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 add async/await back to cancelSession

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 update sessions route

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 use translation string for Sessions

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* test: 💍 fix controller test

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 add scope_id to filters for query

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 remove scope_id and recursive in sessions query

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587

* refactor: 💡 how user query to use global and org scope

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12587
2024-04-12 14:55:33 -04:00
Cameron PereraandZhihe Li be96daa499 Icu 12591 add filtering pagination support for groups (#2205)
* refactor: 💡 move model hook to groups/index

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

* feat: 🎸 add pagination to groups list view

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

* feat: 🎸 add searching to groups

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

* fix: 🐛 update scopeId to scope_id

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

* test: 💍 clean up test refactor

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

* refactor: 💡 update how groupsExisit is set

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

* refactor: 💡 update mirage config for groups

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

* test: 💍 fix failing groups create tests

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

* refactor: 💡 add groups mirage config back

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

* refactor: 💡 use translation string for Groups

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

* refactor: 💡 update how groups are queried

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

* Update ui/admin/tests/acceptance/groups/list-test.js

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>

* refactor: 💡 update groups translation to title_plural

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12591

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2024-04-12 14:55:33 -04:00
Zhihe Li 9f11e8a25d fix: 🐛 Fix setting up indexed db when user logins first time (#2211) 2024-04-12 14:55:33 -04:00
Lisbet Alvarez 8be4e93a3c Icu 12590 add filtering pagination support for roles (#2204)
* feat: 🎸 add search & pagination for roles

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12590
2024-04-12 14:55:33 -04:00
Lisbet Alvarez a78522e09a feat: 🎸 add search & pagination support for users (#2195)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12589
2024-04-12 14:55:33 -04:00
Zhihe LiandLisbet Alvarez a26662eca5 feat: 🎸 Add filtering/pagination for admin ui targets (#2072)
* feat: 🎸 Add filtering/pagination for admin ui targets

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12193

* test: 💍 add acceptance test cases

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12193

* test: 💍 cleanup tests and add controller tests

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12193

---------

Co-authored-by: Lisbet Alvarez <lisbet.alvarez@hashicorp.com>
2024-04-12 14:55:33 -04:00
Zhihe LiandLisbet Alvarez d72fa0d6e3 feat: 🎸 Add indexedDb parser (#2068)
* feat: 🎸 Add indexedDb parser

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12164

---------

Co-authored-by: Lisbet Alvarez <lisbet.alvarez@hashicorp.com>
2024-04-12 14:55:33 -04:00
Zhihe Li bd5e3da1f4 feat: 🎸 Initial setup of IndexedDB (#2064)
* feat: 🎸 Initial setup of IndexedDB

* fix: 🐛 Rethrow errors up from indexed db handler
2024-04-12 14:55:33 -04:00
Dharini Jeeva f74f4d4a20 Target aliases list view (#2225)
target aliases list view
2024-04-11 14:18:38 -07:00
dependabot[bot]andZhihe Li 9bd45164bf chore(deps): bump tar from 6.1.11 to 6.2.1 in /ui/desktop/electron-app (#2230)
Bumps [tar](https://github.com/isaacs/node-tar) from 6.1.11 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.1.11...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

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>
2024-04-11 14:05:30 -04:00
dependabot[bot] 363879c3c3 chore(deps): bump tar from 6.1.15 to 6.2.1 (#2232)
Bumps [tar](https://github.com/isaacs/node-tar) from 6.1.15 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.1.15...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-11 13:41:15 -04:00
Lisbet Alvarez 0b4fd6765e fix: 🐛 modal bubbling up when user has not updated model (#2227)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13106
2024-04-09 12:05:15 -07:00
Dharini Jeeva c8839d6db1 Icu 12933 updates to target model to support aliasing (#2214)
* feat: 🎸 target aliases association API, form creation
2024-04-05 16:35:23 -07:00
Johan Brandhorst-SatzkornandZhihe Li ac5110d94a feat: 🎸 authenticate to ferry on authentication (#2203)
* feat: 🎸 authenticate to ferry on authentication

Replace the 'boundary daemon add-token' command with a simple
'boundary auth-tokens read -id <token_id>', relying on the boundary
CLI to do the work of sending the token to the client and Ferry
daemons.

* refactor: 💡 Use CLI to add tokens to daemons for all platforms

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2024-04-03 12:41:13 -04:00
hashicorp-tsccr[bot]andhashicorp-tsccr[bot] 78bcc103c2 Result of tsccr-helper -log-level=info gha update -latest . (#2210)
Co-authored-by: hashicorp-tsccr[bot] <hashicorp-tsccr[bot]@users.noreply.github.com>
2024-04-02 10:21:39 -07:00
dependabot[bot]andZhihe Li fe0cffdf0b chore(deps): bump express from 4.18.2 to 4.19.2 (#2208)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

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>
2024-03-28 17:45:02 -04:00
Michael Li 79f4c2bc5d test(e2e): Update credential library form behavior (#2207)
* test(e2e): Update credential library form behavior

* fix(e2e):  Fix some locators due to form updates
2024-03-28 15:05:57 -04:00
Devon AndersonandZhihe Li 9843310dfb Icu 12860 retain values in form fields in credential library when switching types (#2169)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12860

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2024-03-27 15:18:58 -04:00
Dharini Jeeva db75b2e292 chore: 🤖 aliases list view (#2197)
* chore: 🤖 aliases list view
2024-03-25 16:54:15 -07:00
Cameron Perera ac1d98bbec fix: 🐛 show change cluster url when no auth methods (#2201)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13133
2024-03-25 14:24:53 -05:00
Cameron Perera 0c6744581d fix: 🐛 remove recursive param from list scopes endpoint (#2199)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12543
2024-03-25 13:47:07 -05:00
hashicorp-copywrite[bot] fb345f045e [COMPLIANCE] Add Copyright and License Headers (#2202)
Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2024-03-25 10:31:51 -07:00
Michael Li b1085def48 test(e2e): Add check for playing back session recording (#2198) 2024-03-25 12:13:27 -04:00
Michael Li 499b0fde31 refact(e2e): Convert to match "create" helper convention (#2200) 2024-03-22 16:22:34 -04:00
dependabot[bot]andCameron Perera bbf9c95267 chore(deps): bump follow-redirects from 1.15.4 to 1.15.6 (#2187)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
2024-03-22 11:07:56 -05:00
Dharini Jeeva 2dbcc1c0af Alias create (#2193)
* feat: 🎸 create alias
2024-03-21 09:50:51 -07:00
Michael Li 6d88206491 refact(e2e): Update create helper methods (#2191) 2024-03-20 09:38:59 -04:00
de20df445c [COMPLIANCE] Add Copyright and License Headers (#2188)
Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Carlos Alcaide Corvo <ccorvo@hashicorp.com>
Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
2024-03-19 21:56:39 -05:00
Michael Li 45ad7a8f3b fix(e2e): Address flake in log in/log out test (#2189) 2024-03-19 16:52:30 -04:00
356 changed files with 11551 additions and 3750 deletions

No files matched your search

+4 -4
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: node-modules-cache
with:
path: "**/node_modules"
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: node-modules-cache
with:
path: "**/node_modules"
@@ -57,7 +57,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: node-modules-cache
with:
path: "**/node_modules"
@@ -80,7 +80,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: node-modules-cache
with:
path: "**/node_modules"
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@592977e6ae857384aa79bb31e7a1d62d63449ec5 # codeql-bundle-v2.16.3
uses: github/codeql-action/init@423a04bb2cb7cd2643007122588f1387778f14d0 # codeql-bundle-v2.16.5
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -54,4 +54,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@592977e6ae857384aa79bb31e7a1d62d63449ec5 # codeql-bundle-v2.16.3
uses: github/codeql-action/analyze@423a04bb2cb7cd2643007122588f1387778f14d0 # codeql-bundle-v2.16.5
+4 -4
View File
@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: node-modules-cache
with:
path: '**/node_modules'
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: node-modules-cache
with:
path: '**/node_modules'
@@ -44,7 +44,7 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: node-modules-cache
with:
path: '**/node_modules'
@@ -63,7 +63,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: node-modules-cache
with:
path: '**/node_modules'
@@ -5,6 +5,8 @@
import sinon from 'sinon';
import { typeOf } from '@ember/utils';
import { resourceNames } from 'api/handlers/client-daemon-handler';
import { singularize } from 'ember-inflector';
/**
* This test helper can be used to help setup your sinon stubs in your tests.
@@ -99,7 +101,8 @@ export default function setupStubs(hooks) {
resource = type.resource;
}
sinon.assert.match(ipcCall.args[1], { resource });
const mappedResourceName = resourceNames[singularize(resource)];
sinon.assert.match(ipcCall.args[1], { resource: mappedResourceName });
});
sinon.restore();
@@ -0,0 +1,35 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
/**
* This test helper can help set up and cleanup the database in your tests.
*
* ```js
* import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
*
* module('Acceptance | my test', function(hooks) {
* setupApplicationTest(hooks);
* setupIndexedDb(hooks);
*
* // add your actual tests here
* });
* ```
*
* @param hooks
*/
export function setupIndexedDb(hooks) {
hooks.beforeEach(function () {
const indexedDb = this.owner.lookup('service:indexed-db');
indexedDb.setup(`test-indexed-db-${Date.now()}`);
});
hooks.afterEach(async function () {
const indexedDb = this.owner.lookup('service:indexed-db');
await indexedDb.db.delete();
});
}
export default setupIndexedDb;
+13 -8
View File
@@ -115,6 +115,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
* @return {Promise} promise
*/
async query(store, schema, query) {
const queryObject = structuredClone(query);
let result;
let data = [];
@@ -128,12 +129,12 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
// Run this loop as long as the response_type is delta,
// which indicates that there are more items in the list
do {
result = await super.query(store, schema, query);
result = await super.query(store, schema, queryObject);
//add the result items to a data array
if (result && result.items) {
data.push(...result.items);
//pass in the list token for subsequent calls to fetch the remaining list items
query.list_token = result.list_token;
queryObject.list_token = result.list_token;
}
} while (result && result.response_type === 'delta');
@@ -211,7 +212,10 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
handleResponse(status, headers, payload, requestData) {
if (this.isInvalid(status, headers, payload)) {
const detailedMessage = this.generatedDetailedMessage(...arguments);
const transformedPayload = this.transformValidationErrors(payload);
const transformedPayload = this.transformValidationErrors(
payload,
status,
);
return new InvalidError(transformedPayload.errors, detailedMessage);
}
return super.handleResponse(status, headers, payload, requestData);
@@ -269,17 +273,18 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
* error format.
*
* @param {object} payload
* @param status
* @return {object}
*/
transformValidationErrors(payload) {
transformValidationErrors(payload, status) {
const fieldErrors = get(payload, 'details.request_fields') || [];
// Normalize the primary error message into "base", which is an error
// that applies to the whole model instance
const baseError = {
isInvalid: true,
status: payload.source,
code: payload.code,
detail: payload.message,
status,
code: payload?.kind,
detail: payload?.message,
source: { pointer: '/data' },
};
// Normalize field-specific errors, if any.
@@ -314,6 +319,6 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
* @return {String} detailed error message
*/
generatedDetailedMessage(status, headers, payload /*, requestData*/) {
return payload.message;
return payload?.message;
}
}
@@ -44,6 +44,12 @@ export default class GeneratedAliasModel extends BaseModel {
})
version;
@attr('string', {
description:
'This is the value referenced by the user that is resolved to the destination id.',
})
value;
@attr('string', {
description: 'This is the id of the resource that this Alias points to.',
})
@@ -73,6 +73,12 @@ export default class GeneratedStorageBucketModel extends BaseModel {
})
region;
@attr('string', {
isNestedAttribute: true,
description: 'The MinIO location server.',
})
endpoint_url;
//this field is not used in the UI, but this is kept here to replicate the API
@attr('string', {
readOnly: true,
@@ -108,4 +108,17 @@ export default class GeneratedTargetModel extends BaseModel {
isNestedAttribute: true,
})
enable_session_recording;
@attr('array', {
description:
'with_aliases specify the aliases that should be created when the target is created. ',
})
with_aliases;
@attr('array', {
readOnly: true,
description:
'with_aliases specify the aliases that should be created when the target is created. ',
})
aliases;
}
@@ -5,43 +5,29 @@
import { inject as service } from '@ember/service';
import { getOwner, setOwner } from '@ember/application';
import { pluralize } from 'ember-inflector';
import { generateMQLExpression } from '../utils/mql-query';
import { paginateResults } from '../utils/paginate-results';
/**
* Not all types are yet supported by the client daemon so we'll
* just whitelist the ones we need to start with. Keys are the resource
* types and the values are the fields that are searchable by the client.
* @type {{session: string[], target: string[]}}
* @type {{session: string[], alias: string[], target: string[]}}
*/
const supportedTypes = {
target: ['id', 'name', 'description', 'address', 'scope_id'],
session: ['id', 'type', 'status', 'endpoint', 'scope_id', 'target_id'],
alias: ['id', 'type', 'value', 'destination_id'],
};
/**
* Takes an array and the current page and pagesize to calculate the correct
* number of results to return to the caller.
*
* @param array
* @param page
* @param pageSize
* @returns {[*]}
* Map the model resource name to the client daemon resource name
* @type {{session: string, alias: string, target: string}}
*/
const paginateResults = (array, page, pageSize) => {
const length = array?.length;
if (!array || length === 0) {
return [];
}
if (!page || !pageSize) {
return array;
}
const offset = (page - 1) * pageSize;
const start = Math.min(length - 1, offset);
const end = Math.min(length, offset + pageSize);
return array.slice(start, end);
export const resourceNames = {
target: 'targets',
session: 'sessions',
alias: 'resolvable-aliases',
};
const fetchControllerData = async (context, next) => {
@@ -112,7 +98,7 @@ export default class ClientDaemonHandler {
query: searchQuery,
auth_token_id,
token,
resource: pluralize(type),
resource: resourceNames[type],
};
let clientDaemonResults = {};
@@ -0,0 +1,172 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { modelIndexes } from 'api/services/indexed-db';
import { inject as service } from '@ember/service';
import { getOwner, setOwner } from '@ember/application';
import { queryIndexedDb } from '../utils/indexed-db-query';
import { paginateResults } from '../utils/paginate-results';
import { hashCode } from '../utils/hash-code';
/**
* Handler to sit in front of the API request layer
* so we can handle any caching with indexedDb first
*/
export default class IndexedDbHandler {
@service indexedDb;
constructor(context) {
setOwner(this, getOwner(context));
}
async request(context, next) {
switch (context.request.op) {
case 'query': {
const { store, data } = context.request;
// TODO: Remove storeToken option as this is a temporary fix for auth-methods.
const {
type,
query,
options: {
pushToStore = true,
peekIndexedDB = false,
storeToken = true,
} = {},
} = data;
const supportedModels = Object.keys(modelIndexes);
const { db: indexedDb } = this.indexedDb ?? {};
// Go through normal flow if we don't yet support the model
// or if we don't have an indexedDb instance
if (!supportedModels.includes(type) || !indexedDb) {
return next(context.request);
}
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
const adapter = store.adapterFor(type);
const schema = store.modelFor(type);
const serializer = store.serializerFor(type);
if (!peekIndexedDB) {
const tokenKey = `${type}-${hashCode(remainingQuery)}`;
let listToken;
if (storeToken) {
listToken = await indexedDb.token.get(tokenKey);
}
let payload;
try {
payload = await adapter.query(store, schema, {
...remainingQuery,
list_token: listToken?.token,
});
} catch (e) {
// If we get an invalid list token, we'll delete the token and
// clear the DB and try again without a token.
if (
e?.errors[0].status === 400 &&
e?.errors[0].code === 'invalid list token'
) {
await indexedDb[type].clear();
// Delete all tokens of the same resource type so we don't keep clearing the DB.
// Even if other tokens may still be valid, we might have cleared data that they
// depended on so we should clear all tokens of the same type when clearing the DB.
const tokenKeys = await indexedDb.token
.where(':id')
.startsWith(type)
.primaryKeys();
await indexedDb.token.bulkDelete(tokenKeys);
payload = await adapter.query(store, schema, {
...remainingQuery,
});
} else {
throw e;
}
}
// Store the token we just got back from the payload if it exists
if (payload.list_token && storeToken) {
await indexedDb.token.put({
id: tokenKey,
token: payload.list_token,
});
}
// Remove any records from the DB if the API indicates they've been deleted
if (payload.removed_ids?.length > 0) {
await indexedDb[type].bulkDelete(payload.removed_ids);
}
// This is a temporary fix of clearing the DB (specifically for auth-methods)
// since we are not storing the token we do not get back a list of removed_ids
// from the API call and we do not want deleted items from showing in list view.
if (!storeToken) {
await indexedDb[type].clear();
}
const normalizedPayload = serializer.normalizeResponse(
store,
schema,
payload,
null,
'query',
);
const { data: payloadData } = normalizedPayload;
// Store the new data we just got back from the API refresh
const items = payloadData.map((datum) =>
this.indexedDb.normalizeData({
data: datum,
cleanData: true,
schema,
serializer,
}),
);
await indexedDb[type].bulkPut(items);
}
const indexedDbResults = await queryIndexedDb(
indexedDb,
type,
queryObj,
);
const dbRecords = paginateResults(indexedDbResults, page, pageSize).map(
(item) =>
this.indexedDb.normalizeData({
data: item,
cleanData: false,
schema,
serializer,
}),
);
// Return the raw data if we don't push to the store.
let records = dbRecords.map((record) => ({
...record.attributes,
id: record.id,
}));
if (pushToStore) {
records = store.push({ data: dbRecords });
}
// Set a meta property on the array to store the total items of the results.
// This isn't conventional but is better than returning an ArrayProxy
// or EmberArray since the ember store query method asserts it has to be an array
// so we can't just return an object.
records.meta = { totalItems: indexedDbResults.length };
return records;
}
default:
return next(context.request);
}
}
}
@@ -0,0 +1,27 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { registerDeprecationHandler } from '@ember/debug';
const deprecationList = ['ember-data:deprecate-early-static'];
export function initialize() {
registerDeprecationHandler((message, options, next) => {
// TODO: Remove deprecation handling for `deprecate-early-static` warning
// once we upgrade to ember-cli-mirage v3.0.0 which fixes this issue.
// More info about this issue here: https://github.com/miragejs/ember-cli-mirage/issues/2441
if (options?.id && deprecationList.includes(options.id)) {
return;
}
if (options?.url) {
message += options.url;
}
next(message, options);
});
}
export default {
initialize,
};
+7 -1
View File
@@ -6,7 +6,13 @@
import GeneratedCredentialStoreModel from '../generated/models/credential-store';
import { equal } from '@ember/object/computed';
export const types = ['static', 'vault'];
export const TYPE_CREDENTIAL_STORE_STATIC = 'static';
export const TYPE_CREDENTIAL_STORE_VAULT = 'vault';
export const TYPES_CREDENTIAL_STORE = Object.freeze([
TYPE_CREDENTIAL_STORE_STATIC,
TYPE_CREDENTIAL_STORE_VAULT,
]);
export default class CredentialStoreModel extends GeneratedCredentialStoreModel {
// =attributes
+16 -3
View File
@@ -5,8 +5,19 @@
import GeneratedHostCatalogModel from '../generated/models/host-catalog';
export const types = ['static', 'plugin'];
export const pluginTypes = ['aws', 'azure'];
export const TYPE_HOST_CATALOG_STATIC = 'static';
export const TYPE_HOST_CATALOG_DYNAMIC = 'plugin';
export const TYPES_HOST_CATALOG = Object.freeze([
TYPE_HOST_CATALOG_STATIC,
TYPE_HOST_CATALOG_DYNAMIC,
]);
export const TYPE_HOST_CATALOG_PLUGIN_AWS = 'aws';
export const TYPE_HOST_CATALOG_PLUGIN_AZURE = 'azure';
export const TYPES_HOST_CATALOG_PLUGIN = [
TYPE_HOST_CATALOG_PLUGIN_AWS,
TYPE_HOST_CATALOG_PLUGIN_AZURE,
];
export default class HostCatalogModel extends GeneratedHostCatalogModel {
// =attributes
@@ -32,7 +43,9 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
* @type {boolean}
*/
get isUnknown() {
return this.isPlugin && !pluginTypes.includes(this.plugin?.name);
return (
this.isPlugin && !TYPES_HOST_CATALOG_PLUGIN.includes(this.plugin?.name)
);
}
/**
+4 -2
View File
@@ -4,7 +4,7 @@
*/
import GeneratedHostSetModel from '../generated/models/host-set';
import { pluginTypes } from './host-catalog';
import { TYPES_HOST_CATALOG_PLUGIN } from './host-catalog';
export default class HostSetModel extends GeneratedHostSetModel {
// =attributes
@@ -30,7 +30,9 @@ export default class HostSetModel extends GeneratedHostSetModel {
* @type {boolean}
*/
get isUnknown() {
return this.isPlugin && !pluginTypes.includes(this.plugin?.name);
return (
this.isPlugin && !TYPES_HOST_CATALOG_PLUGIN.includes(this.plugin?.name)
);
}
/**
+4 -2
View File
@@ -5,7 +5,7 @@
import GeneratedHostModel from '../generated/models/host';
import { attr } from '@ember-data/model';
import { pluginTypes } from './host-catalog';
import { TYPES_HOST_CATALOG_PLUGIN } from './host-catalog';
export default class HostModel extends GeneratedHostModel {
// =attributes
@@ -64,7 +64,9 @@ export default class HostModel extends GeneratedHostModel {
* @type {boolean}
*/
get isUnknown() {
return this.isPlugin && !pluginTypes.includes(this.plugin?.name);
return (
this.isPlugin && !TYPES_HOST_CATALOG_PLUGIN.includes(this.plugin?.name)
);
}
/**
+4 -95
View File
@@ -6,17 +6,11 @@
import GeneratedRoleModel from '../generated/models/role';
import { attr } from '@ember-data/model';
import { inject as service } from '@ember/service';
import { all, resolve } from 'rsvp';
import {
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
export default class RoleModel extends GeneratedRoleModel {
// =services
@service store;
@service resourceFilterStore;
// =attributes
@@ -27,7 +21,7 @@ export default class RoleModel extends GeneratedRoleModel {
* see obvious. Instead, the application layer is expected to load referenced
* users and groups as needed.
*/
@attr('principal-array', {
@attr('array', {
readOnly: true,
emptyArrayIfMissing: true,
})
@@ -55,7 +49,7 @@ export default class RoleModel extends GeneratedRoleModel {
get userIDs() {
return this.principals
.filter(({ type }) => type === 'user')
.map(({ principal_id }) => principal_id);
.map(({ id }) => id);
}
/**
@@ -65,7 +59,7 @@ export default class RoleModel extends GeneratedRoleModel {
get groupIDs() {
return this.principals
.filter(({ type }) => type === 'group')
.map(({ principal_id }) => principal_id);
.map(({ id }) => id);
}
/**
@@ -75,94 +69,9 @@ export default class RoleModel extends GeneratedRoleModel {
get managedGroupIDs() {
return this.principals
.filter(({ type }) => type === 'managed group')
.map(({ principal_id }) => principal_id);
.map(({ id }) => id);
}
/**
* A promise that resolves to an array of User instances.
* When calling this getter, be sure to await resolution
* before interacting with the results.
* @type {Promise[UserModel]}
*/
get users() {
const ids = this.userIDs;
// Role has prinicipal IDs,
// return a promise which resolves model instances for those IDs
if (ids?.length) {
return this.resourceFilterStore
.queryBy('user', { id: ids }, { scope_id: 'global', recursive: true })
.then((models) => models.map((model) => model));
}
// No principal IDs,
// return a promise resolving to an empty array
return resolve([]);
}
/**
* A promise that resolves to an array of Group instances.
* When calling this getter, be sure to await resolution
* before interacting with the results.
* @type {Promise[GroupModel]}
*/
get groups() {
const ids = this.groupIDs;
// Role has prinicipal IDs,
// return a promise which resolves model instances for those IDs
if (ids?.length) {
return this.resourceFilterStore
.queryBy('group', { id: ids }, { scope_id: 'global', recursive: true })
.then((models) => models.map((model) => model));
}
// No principal IDs,
// return a promise resolving to an empty array
return resolve([]);
}
/**
* A promise that resolves to an array of ManagedGroup instances.
* When calling this getter, be sure to await resolution
* before interacting with the results.
* @type {Promise[ManagedGroupModel]}
*/
get managedGroups() {
const ids = this.managedGroupIDs;
// Role has prinicipal IDs
// return a promise which resolves model instances for those IDs
if (ids?.length) {
// Collect all auth methods.
const authMethods = this.resourceFilterStore.queryBy(
'auth-method',
{ type: [TYPE_AUTH_METHOD_OIDC, TYPE_AUTH_METHOD_LDAP] },
{ scope_id: 'global', recursive: true },
);
// For each auth method, query all managed groups with IDs
// that match the role's manage group principal IDs.
const managedGroups = authMethods
.then((methods) =>
all(
methods.map(({ id: auth_method_id }) =>
this.resourceFilterStore
.queryBy('managed-group', { id: ids }, { auth_method_id })
.then((models) => models.map((model) => model)),
),
),
)
// The result is an array of arrays of model instances (grouped by
// auth methods), so these must be flattened.
.then((grouped) => grouped.flat());
return managedGroups;
}
// No principal IDs,
// return a promise resolving to an empty array
return resolve([]);
}
// =methods
/**
+13 -14
View File
@@ -4,13 +4,15 @@
*/
import GeneratedScopeModel from '../generated/models/scope';
import { computed } from '@ember/object';
export const scopeTypes = {
global: 'global',
org: 'org',
project: 'project',
};
export const TYPE_SCOPE_GLOBAL = 'global';
export const TYPE_SCOPE_ORG = 'org';
export const TYPE_SCOPE_PROJECT = 'project';
export const TYPES_SCOPE = Object.freeze([
TYPE_SCOPE_GLOBAL,
TYPE_SCOPE_ORG,
TYPE_SCOPE_PROJECT,
]);
export default class ScopeModel extends GeneratedScopeModel {
// =attributes
@@ -18,9 +20,8 @@ export default class ScopeModel extends GeneratedScopeModel {
/**
* @type {boolean}
*/
@computed('type')
get isGlobal() {
return this.type === scopeTypes.global;
return this.type === TYPE_SCOPE_GLOBAL;
}
// There is only one global scope and it cannot be created by clients,
// thus no set.
@@ -28,23 +29,21 @@ export default class ScopeModel extends GeneratedScopeModel {
/**
* @type {boolean}
*/
@computed('type')
get isOrg() {
return this.type === scopeTypes.org;
return this.type === TYPE_SCOPE_ORG;
}
set isOrg(value) {
if (value) this.type = scopeTypes.org;
if (value) this.type = TYPE_SCOPE_ORG;
}
/**
* @type {boolean}
*/
@computed('type')
get isProject() {
return this.type === scopeTypes.project;
return this.type === TYPE_SCOPE_PROJECT;
}
set isProject(value) {
if (value) this.type = scopeTypes.project;
if (value) this.type = TYPE_SCOPE_PROJECT;
}
/**
+11
View File
@@ -151,6 +151,17 @@ export default class SessionModel extends GeneratedSessionModel {
return this.store.peekRecord('target', this.target_id);
}
/**
* The user associated with this session (if loaded).
* @type {UserModel}
*/
get user() {
if (!this.user_id) {
return null;
}
return this.store.peekRecord('user', this.user_id);
}
// =methods
/**
+10
View File
@@ -9,8 +9,10 @@ export const TYPE_STORAGE_BUCKET_PLUGIN = 'plugin';
export const TYPES_STORAGE_BUCKET = Object.freeze([TYPE_STORAGE_BUCKET_PLUGIN]);
export const TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3 = 'aws';
export const TYPE_STORAGE_BUCKET_PLUGIN_MINIO = 'minio';
export const TYPES_STORAGE_BUCKET_PLUGIN = Object.freeze([
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
]);
export const TYPE_CREDENTIAL_STATIC = 'static';
@@ -75,6 +77,14 @@ export default class StorageBucketModel extends GeneratedStorageBucketModel {
return this.compositeType === TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3;
}
/**
* True if the storage bucket plugin type is MinIO
* @type {boolean}
*/
get isMinIO() {
return this.compositeType === TYPE_STORAGE_BUCKET_PLUGIN_MINIO;
}
/**
* If the storage bucket is a plugin return `plugin.name`,
* otherwise return the storage bucket type.
+6
View File
@@ -135,6 +135,12 @@ export default class TargetModel extends GeneratedTargetModel {
return Boolean(pendingOrActiveSessions.length);
}
get associatedAliases() {
return this.store
.peekAll('alias')
.filter((alias) => alias?.destination_id === this.id);
}
// =methods
/**
+3 -3
View File
@@ -14,7 +14,7 @@ export default class RoleSerializer extends ApplicationSerializer {
* include **only grants** and the version. Normally, grants are not
* serialized.
* If `adapterOptions.principalIDs` is set (to an array of user and
* group IDs), then the payload is serialized via `serializewithPrincipals`.
* group IDs), then the payload is serialized via `serializeWithPrincipals`.
* @override
* @param {Snapshot} snapshot
* @return {object}
@@ -26,7 +26,7 @@ export default class RoleSerializer extends ApplicationSerializer {
serialized = this.serializeWithGrantStrings(snapshot, grantStrings);
const principalIDs = snapshot?.adapterOptions?.principalIDs;
if (principalIDs)
serialized = this.serializewithPrincipals(snapshot, principalIDs);
serialized = this.serializeWithPrincipals(snapshot, principalIDs);
return serialized;
}
@@ -50,7 +50,7 @@ export default class RoleSerializer extends ApplicationSerializer {
* @param {[string]} principalIDs
* @return {object}
*/
serializewithPrincipals(snapshot, principalIDs) {
serializeWithPrincipals(snapshot, principalIDs) {
return {
version: snapshot.attr('version'),
principal_ids: principalIDs,
+37 -6
View File
@@ -4,7 +4,10 @@
*/
import ApplicationSerializer from './application';
import { TYPE_CREDENTIAL_DYNAMIC } from '../models/storage-bucket';
import {
TYPE_CREDENTIAL_DYNAMIC,
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
} from '../models/storage-bucket';
const fieldsByCredentialType = {
static: [
@@ -12,27 +15,55 @@ const fieldsByCredentialType = {
'secret_access_key',
'region',
'disable_credential_rotation',
'endpoint_url',
],
dynamic: [
'role_arn',
'role_external_id',
'role_session_name',
'role_tags',
'disable_credential_rotation',
'region',
'disable_credential_rotation',
'endpoint_url',
],
};
export default class StorageBucketSerializer extends ApplicationSerializer {
/**
* Serializes storage-bucket. ATM just AWS needs specific serialization,
* for the other plugin types use default.
* @param {object} snapshot
* @returns
*/
serialize(snapshot) {
const serialized = super.serialize(...arguments);
const { credentialType } = snapshot.record;
const pluginType = snapshot.record.plugin?.name;
const serialized = super.serialize(...arguments);
// Common for all pluginTypes
if (!snapshot.record.isNew) {
delete serialized.bucket_name;
delete serialized.bucket_prefix;
}
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
serialized.attributes.disable_credential_rotation = true;
if (pluginType === TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3) {
return this.serializeAws(serialized, credentialType);
}
return serialized;
}
/**
* Returns a storage bucket serialized object specific for aws
* @param {object} serialized
* @param {string} credentialType
* @returns
*/
serializeAws(serialized, credentialType) {
if (serialized.attributes) {
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
serialized.attributes.disable_credential_rotation = true;
}
delete serialized.attributes.endpoint_url;
}
return serialized;
}
@@ -45,7 +76,7 @@ export default class StorageBucketSerializer extends ApplicationSerializer {
if (options.isNestedAttribute && json.attributes) {
// The key must be included in the fieldsByType list above
if (!fieldsByCredentialType[credentialType].includes(key))
//API requires these fields to be null
// API requires these fields to be null
json.attributes[key] = null;
}
+21
View File
@@ -44,6 +44,14 @@ export default class TargetSerializer extends ApplicationSerializer {
injectedApplicationCredentialSourceIDs,
);
if (serialized?.with_aliases) {
// API expects scope id along with every alias value
serialized.with_aliases = serialized.with_aliases.map((item) => ({
...item,
scope_id: 'global',
}));
}
// Delete session recording related fields from non-SSH targets
if (type !== TYPE_TARGET_SSH) {
delete serialized?.attributes?.storage_bucket_id;
@@ -112,4 +120,17 @@ export default class TargetSerializer extends ApplicationSerializer {
storage_bucket_id: snapshot?.attr('storage_bucket_id'),
};
}
normalize(typeClass, hash, ...rest) {
const normalizedHash = structuredClone(hash);
const normalized = super.normalize(typeClass, normalizedHash, ...rest);
// Ember data retains the previous entry in the array attr when the updated attr becomes undefined/empty.
// So, we explicitly set the attr to an empty array if the updated attribute is undefined from the API
if (!normalized.data.attributes.aliases) {
normalized.data.attributes.aliases = [];
}
return normalized;
}
}
+147
View File
@@ -0,0 +1,147 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Service from '@ember/service';
import Dexie from 'dexie';
import { typeOf } from '@ember/utils';
// List of resources we support to be cached and searched in
// indexedDb. Any field that can be searched upon should be added to
// this index. Increment the version number of the database whenever
// indexes change.
export const modelIndexes = {
token: '&id, token',
target:
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.address, attributes.scope.scope_id',
session:
'&id, attributes.created_time, attributes.type, attributes.status, attributes.endpoint, attributes.target_id, attributes.user_id, attributes.scope.scope_id',
user: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
group:
'&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
role: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
scope:
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id',
'credential-store':
'&id, attributes.created_time, attributes.name, attributes.description, attributes.type, attributes.scope.scope_id',
'host-catalog':
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id, attributes.plugin.name',
'auth-method':
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.is_primary, attributes.scope.scope_id',
};
export const formatDbName = (userId, clusterUrl) =>
`boundary-${userId}-${clusterUrl}`;
/**
* Recursively copy over the object and convert any booleans to "true" or "false" so they can be indexed.
* Also converts any ember-like arrays back to a standard array.
* @param result
* @param object
* @returns {*}
*/
const convertFields = (result, object) => {
Object.entries(object).forEach(([key, value]) => {
if (typeOf(value) === 'object') {
result[key] = convertFields({}, value);
} else if (typeOf(value) === 'boolean') {
result[key] = value ? 'true' : 'false';
} else if (typeOf(value) === 'array') {
result[key] = [...value];
} else {
result[key] = value;
}
});
return result;
};
/**
* Recursively copy over the object and convert any string "true" or "false" back to booleans.
* Will also convert any array fields back using their model transformation.
* @param result
* @param object
* @param schema
* @param serializer
* @returns {*}
*/
const unconvertFields = (result, object, schema, serializer) => {
Object.entries(object).forEach(([key, value]) => {
if (typeOf(value) === 'object') {
result[key] = unconvertFields({}, value, schema, serializer);
} else if (value === 'true' || value === 'false') {
result[key] = value === 'true';
} else if (typeOf(value) === 'array') {
const attribute = schema.attributes.get(key);
// If attribute type is array, we transform it back to a tracked array
if (attribute?.type === 'array') {
const transformer = serializer.transformFor(attribute.type);
result[key] = transformer.deserialize(value, attribute.options);
return;
}
result[key] = value;
} else {
result[key] = value;
}
});
return result;
};
/**
* Service to encapsulate the IndexedDB implementation. To use this service, call
* `setup` from the application root and
*/
export default class IndexedDbService extends Service {
// =attributes
#db;
get db() {
return this.#db;
}
setup(dbName) {
// Don't run setup again if we already have one or if we didn't get a name
if (this.#db || !dbName) {
return;
}
this.#db = new Dexie(dbName);
this.#db.version(1).stores(modelIndexes);
}
/**
* Cleanup the data by converting all fields to plain javascript objects.
* We will also convert booleans to "false" or "true".
* IndexedDB doesn't support indexes on boolean values so we have to
* convert them to be able to support using boolean values.
*
* Undo the operation by setting `convertFields` to false.
*
* @param {Object} data
* @param {Boolean} cleanData
* @param {Object} schema
* @param {Object} serializer
* @return {{id, type, attributes: {}, relationships: {}}}
*/
normalizeData({ data, cleanData, schema, serializer }) {
if (!data) {
return data;
}
let { id, type, attributes = {}, relationships = {} } = data;
return {
id,
type,
attributes: cleanData
? convertFields({}, attributes)
: unconvertFields({}, attributes, schema, serializer),
relationships: cleanData
? convertFields({}, relationships)
: unconvertFields({}, relationships, schema, serializer),
};
}
}
+4 -1
View File
@@ -6,6 +6,7 @@
import Store, { CacheHandler } from '@ember-data/store';
import RequestManager from '@ember-data/request';
import { LegacyNetworkHandler } from '@ember-data/legacy-compat';
import IndexedDbHandler from 'api/handlers/indexed-db-handler';
export default class extends Store {
requestManager = new RequestManager();
@@ -13,7 +14,9 @@ export default class extends Store {
constructor(args) {
super(args);
this.requestManager.use([LegacyNetworkHandler]);
const indexedDbHandler = new IndexedDbHandler(this);
this.requestManager.use([indexedDbHandler, LegacyNetworkHandler]);
this.requestManager.useCache(CacheHandler);
}
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Transform from '@ember-data/serializer/transform';
import { TrackedArray } from 'tracked-built-ins';
export default class ArrayTransform extends Transform {
deserialize(serialized) {
const arr = new TrackedArray(serialized || []);
return arr;
}
serialize(deserialized) {
const arr = deserialized || [];
return arr;
}
}
@@ -1,26 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Transform from '@ember-data/serializer/transform';
export default class PrincipalArrayTransform extends Transform {
deserialize(serialized) {
const stringValues = serialized || [];
return stringValues.map(({ id: principal_id, scope_id, type }) => ({
principal_id,
scope_id,
type,
}));
}
serialize(deserialized) {
const strings = deserialized || [];
return strings.map(({ principal_id: id, scope_id, type }) => ({
id,
scope_id,
type,
}));
}
}
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
/**
* Takes an object and gets the JSON stringified version of it to be converted
* to a hash code. Taken from https://stackoverflow.com/a/8076436
* @param obj
*/
export const hashCode = (obj) => {
if (!obj) {
return 0;
}
// Use a replacer function to sort the keys of the object so we get the same hashcode
// regardless of the order of the keys.
const jsonString = JSON.stringify(obj, Object.keys(obj).sort());
let hash = 0;
for (let i = 0; i < jsonString.length; i++) {
let code = jsonString.charCodeAt(i);
hash = (hash << 5) - hash + code;
hash = hash & hash; // Convert to 32bit integer
}
// Make sure the hash is positive since we're using it as an ID
return Math.abs(hash);
};
+263
View File
@@ -0,0 +1,263 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { get } from '@ember/object';
import { modelIndexes } from '../services/indexed-db';
/**
* Takes a POJO representing a filter query and builds a query to
* query indexedDb with. Note that the first filter is important as that
* will use the actual index in indexedDb while any subsequent ones will be less efficient.
*
* @example
* const filter = queryIndexedDb(indexedDb, type, {
* search: generated',
* filters: {
* scope_id: {
* logicalOperator: 'and',
* values: [{ notEquals: 'scope1' }, { notEquals: 'scope2' }],
* },
* },
* });
*/
export async function queryIndexedDb(indexedDb, resource, query) {
let { search, filters = {} } = query ?? {};
let filterCollection = indexedDb[resource];
search = getSearchObject(search, resource);
filters = cleanObject(filters);
if (Object.keys(filters).length) {
// Remove the first item to use as the initial collection and then reduce the remaining ones
const entries = Object.entries(filters);
const [firstKey, firstFilterArrayOrObject] = entries.shift();
filterCollection = await entries.reduce(
async (currCollection, [key, filterArrayOrObject]) => {
// Grab the results from the set of one filters to be `AND` together with the rest of the filters
const filterResults = await buildInitialWhereClause({
table: indexedDb[resource],
key,
filterArrayOrObject,
}).primaryKeys();
const resultIdsSet = new Set(filterResults);
return (await currCollection).and((record) => {
return resultIdsSet.has(record.id);
});
},
buildInitialWhereClause({
key: firstKey,
table: indexedDb[resource],
filterArrayOrObject: firstFilterArrayOrObject,
}),
);
}
if (search) {
const { text, fields } = search;
filterCollection = filterCollection.filter((record) =>
fields
.map((key) => get(record, key))
.some((element) => {
if (typeof element === 'string') {
return element?.toLowerCase().includes(text?.toLowerCase());
}
}),
);
}
// OrderBy has to be used on where clauses while if it's a
// collection already we have to use sortBy. We'll also sort by
// descending order based on created time to match the API as any
// "or" clause can change the order
if (filterCollection.orderBy) {
return filterCollection.orderBy(getKey('created_time')).reverse().toArray();
}
return filterCollection.reverse().sortBy(getKey('created_time'));
}
const buildInitialWhereClause = ({ filterArrayOrObject, table, key }) => {
const filterValueArray = Array.isArray(filterArrayOrObject)
? filterArrayOrObject
: filterArrayOrObject.values;
const { logicalOperator } = filterArrayOrObject;
// Short circuit if all the filter types were equals or notEquals since indexedDb
// can more efficiently query if we pass all values at once
const checkAllFilterType = (type) =>
filterValueArray.every((obj) => Object.keys(obj)[0] === type);
if (checkAllFilterType('equals')) {
return table
.where(getKey(key))
.anyOf(filterValueArray.map((obj) => obj['equals']));
}
if (checkAllFilterType('notEquals')) {
return table
.where(getKey(key))
.noneOf(filterValueArray.map((obj) => obj['notEquals']));
}
// Remove the first item to use as the initial where clause and then reduce the remaining ones
const [firstOperation, firstFilterValue] = Object.entries(
filterValueArray[0],
)[0];
// Not all are filters are equals or notEquals,
// manually build the initial where clause and resulting collections
return filterValueArray.slice(1).reduce(
(result, currObj) => {
const [operation, value] = Object.entries(currObj)[0];
return buildIndexedDbCollection({
key: getKey(key),
value,
collection: result,
operation,
logicalOperator,
});
},
buildIndexedDbWhere({
table,
key: getKey(key),
operation: firstOperation,
value: firstFilterValue,
}),
);
};
/**
* Builds the initial where clause for indexed DB to filter on. Note that this makes
* the first filter important as that will do the real filtering from indexed DB while
* any subsequent filter has to be done in memory.
*/
const buildIndexedDbWhere = ({ table, key, value, operation }) => {
const baseWhereClause = table.where(key);
switch (operation) {
case 'contains':
// We return an error due to not being able to support contains on first filter due to performance.
throw new Error('Contains is not supported as the first filter.');
case 'gt':
return baseWhereClause.above(value);
case 'gte':
return baseWhereClause.aboveOrEqual(value);
case 'lt':
return baseWhereClause.below(value);
case 'lte':
return baseWhereClause.belowOrEqual(value);
case 'notEquals':
return baseWhereClause.notEqual(value);
default:
return baseWhereClause.equals(value);
}
};
/**
* Continues building the queries for indexed DB to filter on. Note that these are not true
* indexed DB queries and in fact just standard JS filters for logical "and" queries and parallel queries
* to indexed DB for logical "or" queries that get deduped.
*/
const buildIndexedDbCollection = ({
key,
value,
collection,
operation,
logicalOperator,
}) => {
if (logicalOperator === 'and') {
switch (operation) {
case 'contains':
return collection.and((record) => get(record, key)?.includes(value));
case 'gt':
return collection.and((record) => get(record, key) > value);
case 'gte':
return collection.and((record) => get(record, key) >= value);
case 'lt':
return collection.and((record) => get(record, key) < value);
case 'lte':
return collection.and((record) => get(record, key) <= value);
case 'notEquals':
return collection.and((record) => get(record, key) !== value);
default:
return collection.and((record) => get(record, key) === value);
}
}
switch (operation) {
case 'contains':
// We return an error due to not being able to support contains on this filter due to performance.
throw new Error(
'Contains is not supported as a filter option with "or" operator.',
);
case 'gt':
return collection.or(key).above(value);
case 'gte':
return collection.or(key).aboveOrEqual(value);
case 'lt':
return collection.or(key).below(value);
case 'lte':
return collection.or(key).belowOrEqual(value);
case 'notEquals':
return collection.or(key).notEqual(value);
default:
return collection.or(key).equals(value);
}
};
/**
* Translates the fields passed in from the UI for resources to the keys indexed DB knows them as.
* Usually it's the path of the actual normalized model.
*/
const getKey = (key) => {
switch (key) {
case 'scope_id':
return `attributes.scope.${key}`;
case 'id':
return key;
default:
return `attributes.${key}`;
}
};
/**
* Cleans the object to remove any keys that have a value of an empty array. This makes
* it easier so we don't have to worry about empty arrays getting queried in indexed db.
*/
const cleanObject = (obj) => {
if (!obj) {
return {};
}
return Object.entries(obj).reduce((result, [key, value]) => {
if (Array.isArray(value)) {
if (value.length > 0) {
result[key] = value;
}
} else if (value.values.length > 0) {
result[key] = value;
}
return result;
}, {});
};
/**
* Makes the search object that is expected when a user searches. Uses
* the indexes we setup in indexed db.
*/
const getSearchObject = (text, resource) => {
if (!text) {
return null;
}
return {
text,
fields: modelIndexes[resource]
.split(', ')
.map((index) => index.replace('&', '')),
};
};
+7 -7
View File
@@ -9,8 +9,8 @@
*
* @example
* const filter = generateMQLFilterExpression({
* name: [{ contains: 'search text' }],
* scope: [{ equals: 'scope1'}, { equals: 'scope2' }],
* name: { logicalOperator: 'and', values: [{ notEquals: 'search text' }, { notEquals: 'test' }] },
* scope_id: [{ equals: 'scope1'}, { equals: 'scope2' }],
* });
*
* // (name % "search text") and (scope_id = "scope1" or scope_id = "scope2")
@@ -21,10 +21,10 @@ export function generateMQLFilterExpression(filterObj) {
}
const expressions = Object.entries(filterObj)
.map(([key, filterObjValue]) => {
const filterValueArray = Array.isArray(filterObjValue)
? filterObjValue
: filterObjValue.values;
.map(([key, filterArrayOrObject]) => {
const filterValueArray = Array.isArray(filterArrayOrObject)
? filterArrayOrObject
: filterArrayOrObject.values;
let clauses;
const filterValues = filterValueArray.map((filterObjValue) => {
@@ -53,7 +53,7 @@ export function generateMQLFilterExpression(filterObj) {
});
// Default to `or` if we don't have a logical operator
const { logicalOperator } = filterObjValue;
const { logicalOperator } = filterArrayOrObject;
if (logicalOperator === 'and') {
clauses = and(filterValues);
} else {
@@ -0,0 +1,29 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
/**
* Takes an array and the current page and pagesize to calculate the correct
* number of results to return to the caller.
*
* @param array
* @param page
* @param pageSize
* @returns {[*]}
*/
export const paginateResults = (array, page, pageSize) => {
const length = array?.length;
if (!array || length === 0) {
return [];
}
if (!page || !pageSize) {
return array;
}
const offset = (page - 1) * pageSize;
const start = Math.min(length - 1, offset);
const end = Math.min(length, offset + pageSize);
return array.slice(start, end);
};
@@ -3,4 +3,4 @@
* SPDX-License-Identifier: MPL-2.0
*/
export { default } from 'api/handlers/client-daemon-handler';
export { default, resourceNames } from 'api/handlers/client-daemon-handler';
@@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
export { default, initialize } from 'api/initializers/deprecations';
@@ -3,4 +3,4 @@
* SPDX-License-Identifier: MPL-2.0
*/
export { default } from 'api/transforms/principal-array';
export { default } from 'api/services/indexed-db';
@@ -3,4 +3,4 @@
* SPDX-License-Identifier: MPL-2.0
*/
export { default } from 'api/utils/mql-query';
export { default } from 'api/transforms/array';
+4 -36
View File
@@ -117,28 +117,7 @@ function routes() {
);
// Auth & IAM resources
this.get(
'/auth-methods',
({ authMethods }, { queryParams: { scope_id, recursive, filter } }) => {
let resultSet;
if (recursive && scope_id === 'global') {
resultSet = authMethods.all();
} else if (recursive) {
resultSet = authMethods.where((authMethod) => {
const authMethodModel = authMethods.find(authMethod.id);
return (
authMethod.scopeId === scope_id ||
authMethodModel?.scope?.scope?.id === scope_id
);
});
} else {
resultSet = authMethods.where(
(authMethod) => authMethod.scopeId === scope_id,
);
}
return resultSet.filter(makeBooleanFilter(filter));
},
);
this.get('/auth-methods');
this.post('/auth-methods', function ({ authMethods }) {
const attrs = this.normalizedRequestAttrs();
if (attrs.type === 'oidc') {
@@ -324,9 +303,7 @@ function routes() {
);
// IAM: Roles
this.get('/roles', ({ roles }, { queryParams: { scope_id: scopeId } }) => {
return roles.where({ scopeId });
});
this.get('/roles');
this.post('/roles');
this.get('/roles/:id');
this.patch('/roles/:id');
@@ -405,12 +382,7 @@ function routes() {
// Other resources
// host-catalog
this.get(
'/host-catalogs',
({ hostCatalogs }, { queryParams: { scope_id: scopeId } }) => {
return hostCatalogs.where({ scopeId });
},
);
this.get('/host-catalogs');
this.post(
'/host-catalogs',
function ({ hostCatalogs }, { queryParams: { plugin_name } }) {
@@ -612,11 +584,7 @@ function routes() {
// credential-stores
this.get(
'/credential-stores',
({ credentialStores }, { queryParams: { scope_id: scopeId } }) =>
credentialStores.where({ scopeId }),
);
this.get('/credential-stores');
this.get('/credential-stores/:id');
this.post('/credential-stores');
@@ -11,6 +11,7 @@ import {
TYPES_STORAGE_BUCKET as types,
TYPES_STORAGE_BUCKET_PLUGIN,
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
} from 'api/models/storage-bucket';
// Represents known plugin types, except "foobar" which models the possibility
@@ -50,6 +51,10 @@ export default factory.extend({
)}`,
disable_credential_rotation: faker.datatype.boolean(),
};
case TYPE_STORAGE_BUCKET_PLUGIN_MINIO:
return {
endpoint_url: faker.internet.url(),
};
}
},
});
+8
View File
@@ -82,6 +82,14 @@ export default factory.extend({
});
}
const aliases = server.schema.aliases.all().models;
const randomlySelectedAliases =
aliases.length === 0 ? undefined : faker.helpers.arrayElement(aliases);
if (randomlySelectedAliases) {
target.update({
aliases: randomlySelectedAliases.aliases,
});
}
target.update({
hostSets: randomlySelectedHostSets,
brokeredCredentialSourceIds: [
+12 -3
View File
@@ -108,9 +108,6 @@ export default function (server) {
server.createList('policy', 3, { scope: globalScope });
server.createList('policy', 3, { scope: orgScope });
// Aliases
server.createList('alias', 3, { scope: globalScope });
// Other resources
server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => {
server.createList('host-catalog', 8, { scope }, 'withChildren');
@@ -127,6 +124,18 @@ export default function (server) {
server.createList('role', 3, { scope });
});
// Aliases
const aliasDestinationTarget = server.schema.targets.all().models[0];
const { id: destination_id } = aliasDestinationTarget;
const aliases = server.createList('alias', 4, {
scope: globalScope,
destination_id,
});
aliasDestinationTarget.update({
aliases: aliases.map((alias) => ({ id: alias.id, value: alias.value })),
});
// Workers
server.createList('worker', 3, { scope: globalScope });
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
modelName: 'alias',
});
+18 -1
View File
@@ -20,7 +20,7 @@ export default ApplicationSerializer.extend({
this,
arguments,
);
const { hostSets } = this.schema;
const { hostSets, aliases } = this.schema;
if (model.hostSetIds?.length) {
json.host_sources = model.hostSetIds.map((host_set_id) => {
const hostSet = hostSets.find(host_set_id);
@@ -28,6 +28,23 @@ export default ApplicationSerializer.extend({
return { id: host_set_id, host_catalog_id };
});
}
// After we clear an associated alias, we try to mimic the BE logic
// by manually filter out the alias id from the target model
const associatedAliases = aliases
.all()
.models.reduce((arr, { destinationId, id }) => {
// We iterate through the aliases list and
// find the cleared alias by looking for camelCase destinationId since there is no other way to find the impacted alias
if (destinationId !== null) arr.push(id);
return arr;
}, []);
json.aliases = model.aliases?.filter((alias) =>
associatedAliases.includes(alias.id),
);
return json;
},
});
+2 -1
View File
@@ -23,6 +23,7 @@
"doc:toc": "doctoc README.md"
},
"dependencies": {
"dexie": "^3.2.4",
"ember-auto-import": "^2.6.3",
"ember-can": "^4.2.0",
"ember-cli-babel": "^7.26.10",
@@ -30,7 +31,6 @@
"ember-cli-mirage": "^2.4.0",
"ember-data": "~4.12.0",
"ember-fetch": "^8.1.1",
"ember-auto-import": "^2.6.3",
"tracked-built-ins": "^3.1.1"
},
"devDependencies": {
@@ -64,6 +64,7 @@
"eslint-plugin-ember": "^11.11.1",
"eslint-plugin-n": "^16.3.1",
"eslint-plugin-qunit": "^8.0.1",
"@faker-js/faker": "^8.0.2",
"js-bexpr": "hashicorp/js-bexpr#9b4a4b54d85eba67fdfc0990133d1518d890b1e1",
"loader.js": "^4.7.0",
"prettier": "^3.0.0",
@@ -28,6 +28,10 @@ module.exports = function (environment) {
host: '',
namespace: 'v1',
},
'ember-cli-mirage': {
directory: './mirage',
},
};
if (environment === 'development') {
-10
View File
@@ -1,10 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { createServer } from 'miragejs';
export default function (config) {
return createServer({ ...config });
}
@@ -1,12 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
export default function (/* server */) {
/*
Seed your development database using your factories.
This data will not be loaded in your tests.
*/
// server.createList('post', 10);
}
@@ -1,8 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { JSONAPISerializer } from 'miragejs';
export default JSONAPISerializer.extend({});
@@ -100,7 +100,7 @@ module('Unit | Adapter | application', function (hooks) {
test('it can request records through the store from a specified scope', async function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
this.server.get('/v1/groups', (_, { queryParams: { scope_id } }) => {
this.server.get('/groups', (_, { queryParams: { scope_id } }) => {
assert.strictEqual(
scope_id,
'p_456',
@@ -0,0 +1,42 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Application from '@ember/application';
import config from 'dummy/config/environment';
import { initialize } from 'dummy/initializers/deprecations';
import { module, test } from 'qunit';
import Resolver from 'ember-resolver';
import { run } from '@ember/runloop';
module('Unit | Initializer | deprecations', function (hooks) {
hooks.beforeEach(function () {
this.TestApplication = class TestApplication extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
};
this.TestApplication.initializer({
name: 'initializer under test',
initialize,
});
this.application = this.TestApplication.create({
autoboot: false,
});
});
hooks.afterEach(function () {
run(this.application, 'destroy');
});
// TODO: Replace this with your real tests.
test('it works', async function (assert) {
await this.application.boot();
assert.ok(true);
});
});
@@ -29,19 +29,16 @@ module('Unit | Model | auth method', function (hooks) {
test('it has a `changeState` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/auth-methods/123abc:change-state',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
attributes: {
state: 'foobar',
},
version: 1,
});
return { id: '123abc' };
},
);
this.server.post('/auth-methods/123abc:change-state', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
attributes: {
state: 'foobar',
},
version: 1,
});
return { id: '123abc' };
});
store.push({
data: {
id: '123abc',
+8 -8
View File
@@ -80,7 +80,7 @@ module('Unit | Model | base', function (hooks) {
attributes: {},
},
});
this.server.patch('/v1/users/1', () => {
this.server.patch('users/1', () => {
assert.ok(true, 'Correctly scoped update record URL was requested.');
return { id: 1 };
});
@@ -112,17 +112,17 @@ module('Unit | Model | base', function (hooks) {
assert.expect(3);
const store = this.owner.lookup('service:store');
const model = store.createRecord('user', { scopeID: 'o_123' });
this.server.post('/v1/users', () => {
this.server.post('users', () => {
assert.ok(true, 'Correctly scoped create record URL was requested.');
return { id: 'u_123' };
});
await model.save();
this.server.patch('/v1/users/u_123', () => {
this.server.patch('users/u_123', () => {
assert.ok(true, 'Correctly scoped update record URL was requested.');
return { id: 'u_123' };
});
await model.save();
this.server.delete('/v1/users/u_123', () => {
this.server.delete('users/u_123', () => {
assert.ok(true, 'Correctly scoped delete record URL was requested.');
return {};
});
@@ -135,17 +135,17 @@ module('Unit | Model | base', function (hooks) {
const customScopeID = 'global';
const scope = { scope_id: 'o_123' };
const model = store.createRecord('user', { scope });
this.server.post('/v1/users', () => {
this.server.post('users', () => {
assert.ok(true, 'Correctly scoped create record URL was requested.');
return { id: 'u_123' };
});
await model.save({ adapterOptions: { scopeID: customScopeID } });
this.server.patch('/v1/users/u_123', () => {
this.server.patch('users/u_123', () => {
assert.ok(true, 'Correctly scoped update record URL was requested.');
return { id: 'u_123' };
});
await model.save({ adapterOptions: { scopeID: customScopeID } });
this.server.delete('/v1/users/u_123', () => {
this.server.delete('users/u_123', () => {
assert.ok(true, 'Correctly scoped delete record URL was requested.');
return {};
});
@@ -158,7 +158,7 @@ module('Unit | Model | base', function (hooks) {
const method = 'my-custom-method';
const scope = { scope_id: 'o_123' };
const model = store.createRecord('user', { scope });
this.server.post(`/v1/users:${method}`, () => {
this.server.post(`users:${method}`, () => {
assert.ok(true, 'Correctly scoped create record URL was requested.');
return { id: 'u_123' };
});
+3 -3
View File
@@ -63,7 +63,7 @@ module('Unit | Model | group', function (hooks) {
test('it has an `addMembers` method that groups a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post('/v1/groups/123abc:add-members', (schema, request) => {
this.server.post('/groups/123abc:add-members', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
member_ids: ['123_abc', 'foobar'],
@@ -94,7 +94,7 @@ module('Unit | Model | group', function (hooks) {
test('it has a `removeMembers` method that groups a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post('/v1/groups/123abc:remove-members', (schema, request) => {
this.server.post('/groups/123abc:remove-members', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
member_ids: ['1', '3'],
@@ -125,7 +125,7 @@ module('Unit | Model | group', function (hooks) {
test('it has a `removeMember` method that deletes a single member set using `removeMembers` method', async function (assert) {
assert.expect(1);
this.server.post('/v1/groups/123abc:remove-members', (schema, request) => {
this.server.post('/groups/123abc:remove-members', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
member_ids: ['3'],
@@ -13,7 +13,7 @@ module('Unit | Model | host set', function (hooks) {
test('it has an `addHosts` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post('/v1/host-sets/123abc:add-hosts', (schema, request) => {
this.server.post('/host-sets/123abc:add-hosts', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
host_ids: ['123_abc', 'foobar'],
@@ -44,7 +44,7 @@ module('Unit | Model | host set', function (hooks) {
test('it has an `addHost` method that adds a single host using `addHosts` method', async function (assert) {
assert.expect(1);
this.server.post('/v1/host-sets/123abc:add-hosts', (schema, request) => {
this.server.post('/host-sets/123abc:add-hosts', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
host_ids: ['foobar'],
@@ -75,7 +75,7 @@ module('Unit | Model | host set', function (hooks) {
test('it has a `removeHosts` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post('/v1/host-sets/123abc:remove-hosts', (schema, request) => {
this.server.post('/host-sets/123abc:remove-hosts', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
host_ids: ['3'],
@@ -106,7 +106,7 @@ module('Unit | Model | host set', function (hooks) {
test('it has a `removeHost` method that removes a single host using `removeHosts` method', async function (assert) {
assert.expect(1);
this.server.post('/v1/host-sets/123abc:remove-hosts', (schema, request) => {
this.server.post('/host-sets/123abc:remove-hosts', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
host_ids: ['3'],
+18 -24
View File
@@ -19,7 +19,7 @@ module('Unit | Model | role', function (hooks) {
test('it has a `saveGrantStrings` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post('/v1/roles/123abc:set-grants', (schema, request) => {
this.server.post('/roles/123abc:set-grants', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
grant_strings: ['foo', 'bar'],
@@ -50,7 +50,7 @@ module('Unit | Model | role', function (hooks) {
test('it has an `addPrincipals` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post('/v1/roles/123abc:add-principals', (schema, request) => {
this.server.post('/roles/123abc:add-principals', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
principal_ids: ['123_abc', 'foobar'],
@@ -85,17 +85,14 @@ module('Unit | Model | role', function (hooks) {
test('it has a `removePrincipals` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/roles/123abc:remove-principals',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
principal_ids: ['1', '3', '5'],
version: 1,
});
return { id: '123abc' };
},
);
this.server.post('/roles/123abc:remove-principals', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
principal_ids: ['1', '3', '5'],
version: 1,
});
return { id: '123abc' };
});
const store = this.owner.lookup('service:store');
store.push({
data: {
@@ -123,17 +120,14 @@ module('Unit | Model | role', function (hooks) {
test('it has a `removePrincipal` method that deletes a single principal using `removePrincipals` method', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/roles/123abc:remove-principals',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
principal_ids: ['3'],
version: 1,
});
return { id: '123abc' };
},
);
this.server.post('/roles/123abc:remove-principals', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
principal_ids: ['3'],
version: 1,
});
return { id: '123abc' };
});
const store = this.owner.lookup('service:store');
store.push({
data: {
+3 -3
View File
@@ -13,7 +13,7 @@ module('Unit | Model | scope', function (hooks) {
test('it may have a scope fragment', async function (assert) {
const store = this.owner.lookup('service:store');
this.server.get('/v1/scopes', () => ({
this.server.get('/scopes', () => ({
items: [
{ id: 'global', type: 'global' },
{ id: 'o_1', type: 'org', scope: { scope_id: 'global' } },
@@ -72,7 +72,7 @@ module('Unit | Model | scope', function (hooks) {
test('it has an `attachStoragePolicy` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/scopes/123abc:attach-storage-policy',
'/scopes/123abc:attach-storage-policy',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
@@ -106,7 +106,7 @@ module('Unit | Model | scope', function (hooks) {
test('it has a `detachStoragePolicy` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/scopes/123abc:detach-storage-policy',
'/scopes/123abc:detach-storage-policy',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
@@ -72,4 +72,88 @@ module('Unit | Model | session', function (hooks) {
assert.false(modelA.isUnknownStatus);
assert.true(modelB.isUnknownStatus);
});
test('it has target property and returns the null with no target_id set', function (assert) {
const store = this.owner.lookup('service:store');
store.push({
data: {
id: 's_123',
type: 'session',
attributes: { target_id: null },
},
});
const session = store.peekRecord('session', 's_123');
assert.strictEqual(session.target, null);
});
test('it has target property and returns the correct target', function (assert) {
const store = this.owner.lookup('service:store');
const targetId = 'ttcp_123';
store.push({
data: {
id: targetId,
type: 'target',
attributes: {
name: 'admin',
scope: {
scope_id: 'o_1',
type: 'scope',
},
},
},
});
store.push({
data: {
id: 's_123',
type: 'session',
attributes: { target_id: targetId },
},
});
const session = store.peekRecord('session', 's_123');
assert.strictEqual(session.target.id, targetId);
});
test('it has user property and returns the null with no user_id set', function (assert) {
const store = this.owner.lookup('service:store');
store.push({
data: {
id: 's_123',
type: 'session',
attributes: { user_id: null },
},
});
const session = store.peekRecord('session', 's_123');
assert.strictEqual(session.user, null);
});
test('it has user property and returns the correct user', function (assert) {
const store = this.owner.lookup('service:store');
const userId = 'u_123';
store.push({
data: {
id: userId,
type: 'user',
attributes: {
name: 'admin',
scope: {
scope_id: 'o_1',
type: 'scope',
},
},
},
});
store.push({
data: {
id: 's_123',
type: 'session',
attributes: { user_id: userId },
},
});
const session = store.peekRecord('session', 's_123');
assert.strictEqual(session.user.id, userId);
});
});
+13 -16
View File
@@ -79,17 +79,14 @@ module('Unit | Model | target', function (hooks) {
test('it has an `addHostSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/targets/123abc:add-host-sources',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
host_source_ids: ['123_abc', 'foobar'],
version: 1,
});
return { id: '123abc' };
},
);
this.server.post('/targets/123abc:add-host-sources', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
host_source_ids: ['123_abc', 'foobar'],
version: 1,
});
return { id: '123abc' };
});
const store = this.owner.lookup('service:store');
store.push({
data: {
@@ -117,7 +114,7 @@ module('Unit | Model | target', function (hooks) {
test('it has a `removeHostSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/targets/123abc:remove-host-sources',
'/targets/123abc:remove-host-sources',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
@@ -154,7 +151,7 @@ module('Unit | Model | target', function (hooks) {
test('it has a `removeHostSource` method that deletes a single host set using `removeHostSources` method', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/targets/123abc:remove-host-sources',
'/targets/123abc:remove-host-sources',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
@@ -283,7 +280,7 @@ module('Unit | Model | target', function (hooks) {
test('it has an `addBrokeredCredentialSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/targets/123abc:add-credential-sources',
'/targets/123abc:add-credential-sources',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
@@ -317,7 +314,7 @@ module('Unit | Model | target', function (hooks) {
test('it has a `removeBrokeredCredentialSources` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/targets/123abc:remove-credential-sources',
'/targets/123abc:remove-credential-sources',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
@@ -351,7 +348,7 @@ module('Unit | Model | target', function (hooks) {
test('it has a `removeBrokeredCredentialSource` method that deletes a single credential library using `removeBrokeredCredentialSources` method', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/targets/123abc:remove-credential-sources',
'/targets/123abc:remove-credential-sources',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
+4 -4
View File
@@ -13,7 +13,7 @@ module('Unit | Model | user', function (hooks) {
test('it has an `addAccounts` method that users a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post('/v1/users/123abc:add-accounts', (schema, request) => {
this.server.post('/users/123abc:add-accounts', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
account_ids: ['123_abc', 'foobar'],
@@ -44,7 +44,7 @@ module('Unit | Model | user', function (hooks) {
test('it has a `removeAccounts` method that users a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => {
this.server.post('/users/123abc:remove-accounts', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
account_ids: ['1', '3'],
@@ -75,7 +75,7 @@ module('Unit | Model | user', function (hooks) {
test('it has a `removeAccount` method that deletes a single account set using `removeAccounts` method', async function (assert) {
assert.expect(1);
this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => {
this.server.post('/users/123abc:remove-accounts', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
account_ids: ['3'],
@@ -122,7 +122,7 @@ module('Unit | Model | user', function (hooks) {
test('it defaults to null for readonly attributes when response does not have fields', async function (assert) {
assert.expect(3);
this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => {
this.server.post('/users/123abc:remove-accounts', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
account_ids: ['3'],
+1 -1
View File
@@ -15,7 +15,7 @@ module('Unit | Model | worker', function (hooks) {
assert.expect(2);
const scopeId = 'global';
const workerGeneratedAuthToken = 'token';
this.server.post('/v1/workers:create:worker-led', (schema, request) => {
this.server.post('/workers:create:worker-led', (schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
scope_id: scopeId,
@@ -285,6 +285,7 @@ module('Unit | Serializer | application', function (hooks) {
type: 'target',
attributes: {
authorized_actions: [],
aliases: [],
name: 'Target 1',
scope: { id: 'o_123', scope_id: 'o_123' },
host_sources: [],
@@ -379,7 +379,7 @@ module('Unit | Serializer | auth method', function (hooks) {
const dryRun = true;
const issuer = 'http://www.example.net';
const maxAge = 500;
this.server.get('/v1/auth-methods/oidc123', () => ({
this.server.get('/auth-methods/oidc123', () => ({
attributes: {
account_claim_maps: ['from=to', 'foo=bar'],
claims_scopes: ['profile', 'email'],
@@ -447,7 +447,7 @@ module('Unit | Serializer | auth method', function (hooks) {
const clientCerticate = 'certificate-1234';
const startTls = true;
const insecureTls = false;
this.server.get('/v1/auth-methods/ldap123', () => ({
this.server.get('/auth-methods/ldap123', () => ({
attributes: {
account_attribute_maps: ['from=to', 'foo=bar'],
urls: [ldapUrl],
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
TYPE_STORAGE_BUCKET_PLUGIN,
TYPE_CREDENTIAL_STATIC,
TYPE_CREDENTIAL_DYNAMIC,
@@ -15,15 +16,6 @@ import {
module('Unit | Serializer | storage bucket', function (hooks) {
setupTest(hooks);
test('it serializes records', function (assert) {
let store = this.owner.lookup('service:store');
let record = store.createRecord('storage-bucket', {});
let serializedRecord = record.serialize();
assert.ok(serializedRecord);
});
test('it serializes a new static aws plugin as expected', async function (assert) {
const store = this.owner.lookup('service:store');
const record = store.createRecord('storage-bucket', {
@@ -70,6 +62,47 @@ module('Unit | Serializer | storage bucket', function (hooks) {
assert.deepEqual(record.serialize(), expectedResult);
});
test('it serializes a new minio (static by default) plugin as expected', async function (assert) {
const store = this.owner.lookup('service:store');
const record = store.createRecord('storage-bucket', {
compositeType: TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
credentialType: TYPE_CREDENTIAL_STATIC,
name: 'minio',
description: 'this has a minio plugin',
bucket_name: 'bucketname',
bucket_prefix: 'bucketprefix',
worker_filter: 'workerfilter',
endpoint_url: 'http://hashicorp.com',
region: 'eu-west-1',
access_key_id: 'foobars',
secret_access_key: 'testing',
disable_credential_rotation: true,
});
const expectedResult = {
name: 'minio',
description: 'this has a minio plugin',
type: TYPE_STORAGE_BUCKET_PLUGIN,
bucket_name: 'bucketname',
bucket_prefix: 'bucketprefix',
worker_filter: 'workerfilter',
attributes: {
region: 'eu-west-1',
disable_credential_rotation: true,
endpoint_url: 'http://hashicorp.com',
role_arn: null,
role_external_id: null,
role_session_name: null,
role_tags: null,
secrets_hmac: null,
},
secrets: {
access_key_id: 'foobars',
secret_access_key: 'testing',
},
};
assert.deepEqual(record.serialize(), expectedResult);
});
test('it serializes a new aws plugin with dynamic credentials as expected', async function (assert) {
const store = this.owner.lookup('service:store');
const record = store.createRecord('storage-bucket', {
@@ -126,6 +159,7 @@ module('Unit | Serializer | storage bucket', function (hooks) {
bucket_name: 'bucketname',
bucket_prefix: 'bucketprefix',
worker_filter: 'workerfilter',
plugin: { name: 'aws' },
region: 'eu-west-1',
access_key_id: 'foobars',
secret_access_key: 'testing',
@@ -183,6 +217,7 @@ module('Unit | Serializer | storage bucket', function (hooks) {
bucket_name: 'bucketname',
bucket_prefix: 'bucketprefix',
worker_filter: 'workerfilter',
plugin: { name: 'aws' },
region: 'eu-west-1',
disable_credential_rotation: true,
access_key_id: 'test',
@@ -222,6 +257,61 @@ module('Unit | Serializer | storage bucket', function (hooks) {
assert.deepEqual(serializedRecord, expectedResult);
});
test('it serializes when updating a MinIO (static credential by default) storage bucket correctly', async function (assert) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('storage-bucket');
store.push({
data: {
id: '3',
type: 'storage-bucket',
attributes: {
type: TYPE_STORAGE_BUCKET_PLUGIN,
plugin: { name: 'minio' },
name: 'minio',
description: 'this has a minio plugin',
bucket_name: 'bucketname',
bucket_prefix: 'bucketprefix',
worker_filter: 'workerfilter',
endpoint_url: 'http://hashicorp.com',
region: 'eu-west-1',
disable_credential_rotation: true,
access_key_id: 'test',
secret_access_key: 'test',
version: 1,
},
},
});
const record = store.peekRecord('storage-bucket', '3');
record.description = 'This is new description';
record.name = 'This is the new name';
record.endpoint_url = 'http://developer.hashicorp.com';
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
const expectedResult = {
name: 'This is the new name',
description: 'This is new description',
type: TYPE_STORAGE_BUCKET_PLUGIN,
worker_filter: 'workerfilter',
attributes: {
region: 'eu-west-1',
disable_credential_rotation: true,
endpoint_url: 'http://developer.hashicorp.com',
role_arn: null,
role_external_id: null,
role_session_name: null,
role_tags: null,
secrets_hmac: null,
},
secrets: {
access_key_id: 'test',
secret_access_key: 'test',
},
version: 1,
};
assert.deepEqual(serializedRecord, expectedResult);
});
test('it normalizes correctly', function (assert) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('storage-bucket');
@@ -36,6 +36,7 @@ module('Unit | Serializer | target', function (hooks) {
address: '0.0.0.0',
enable_session_recording: false,
storage_bucket_id: null,
with_aliases: [{ value: 'a' }],
});
const snapshot = record._createSnapshot();
snapshot.adapterOptions = {};
@@ -51,6 +52,7 @@ module('Unit | Serializer | target', function (hooks) {
worker_filter: null,
egress_worker_filter: null,
ingress_worker_filter: null,
with_aliases: [{ value: 'a', scope_id: 'global' }],
attributes: {
default_port: 1234,
default_client_port: 4321,
@@ -85,6 +87,7 @@ module('Unit | Serializer | target', function (hooks) {
enable_session_recording: false,
storage_bucket_id: null,
address: '0.0.0.0',
with_aliases: [{ value: 'www.test.com' }],
});
const snapshot = record._createSnapshot();
snapshot.adapterOptions = {};
@@ -100,6 +103,7 @@ module('Unit | Serializer | target', function (hooks) {
worker_filter: null,
egress_worker_filter: null,
ingress_worker_filter: null,
with_aliases: [{ value: 'www.test.com', scope_id: 'global' }],
attributes: {
default_port: 1234,
default_client_port: 4321,
@@ -189,6 +193,7 @@ module('Unit | Serializer | target', function (hooks) {
enable_session_recording: true,
storage_bucket_id: 'bucketID',
default_port: 1234,
with_aliases: [],
scope: {
scope_id: 'org_1',
},
@@ -206,6 +211,7 @@ module('Unit | Serializer | target', function (hooks) {
worker_filter: null,
egress_worker_filter: null,
ingress_worker_filter: null,
with_aliases: [],
attributes: {
default_port: 1234,
default_client_port: null,
@@ -226,6 +232,7 @@ module('Unit | Serializer | target', function (hooks) {
type: TYPE_TARGET_SSH,
worker_filter: 'worker',
default_port: 1234,
with_aliases: [],
scope: {
scope_id: 'org_1',
},
@@ -243,6 +250,7 @@ module('Unit | Serializer | target', function (hooks) {
worker_filter: 'worker',
egress_worker_filter: null,
ingress_worker_filter: null,
with_aliases: [],
attributes: {
default_port: 1234,
default_client_port: null,
@@ -263,6 +271,7 @@ module('Unit | Serializer | target', function (hooks) {
type: TYPE_TARGET_SSH,
egress_worker_filter: 'egress worker',
default_port: 1234,
with_aliases: [],
scope: {
scope_id: 'org_1',
},
@@ -280,6 +289,7 @@ module('Unit | Serializer | target', function (hooks) {
worker_filter: null,
egress_worker_filter: 'egress worker',
ingress_worker_filter: null,
with_aliases: [],
attributes: {
default_port: 1234,
default_client_port: null,
@@ -300,6 +310,7 @@ module('Unit | Serializer | target', function (hooks) {
type: TYPE_TARGET_SSH,
ingress_worker_filter: 'ingress worker',
default_port: 1234,
with_aliases: [],
scope: {
scope_id: 'org_1',
},
@@ -317,6 +328,7 @@ module('Unit | Serializer | target', function (hooks) {
worker_filter: null,
egress_worker_filter: null,
ingress_worker_filter: 'ingress worker',
with_aliases: [],
attributes: {
default_port: 1234,
default_client_port: null,
@@ -352,6 +364,7 @@ module('Unit | Serializer | target', function (hooks) {
id: '1',
type: 'target',
attributes: {
aliases: [],
authorized_actions: [],
name: 'Target 1',
host_sources: [
@@ -388,6 +401,7 @@ module('Unit | Serializer | target', function (hooks) {
id: '1',
type: 'target',
attributes: {
aliases: [],
brokered_credential_source_ids: [],
injected_application_credential_source_ids: [],
authorized_actions: [],
@@ -420,6 +434,7 @@ module('Unit | Serializer | target', function (hooks) {
id: '1',
type: 'target',
attributes: {
aliases: [],
authorized_actions: [],
name: 'Target 1',
scope: { id: 'o_123', scope_id: 'o_123' },
@@ -452,6 +467,7 @@ module('Unit | Serializer | target', function (hooks) {
id: '1',
type: 'target',
attributes: {
aliases: [],
authorized_actions: [],
name: 'Target 1',
host_sources: [],
@@ -484,6 +500,7 @@ module('Unit | Serializer | target', function (hooks) {
id: '1',
type: 'target',
attributes: {
aliases: [],
authorized_actions: [],
name: 'Target 1',
host_sources: [],
@@ -516,6 +533,7 @@ module('Unit | Serializer | target', function (hooks) {
id: '1',
type: 'target',
attributes: {
aliases: [],
authorized_actions: [],
name: 'Target 1',
host_sources: [],
@@ -549,6 +567,7 @@ module('Unit | Serializer | target', function (hooks) {
id: '1',
type: 'target',
attributes: {
aliases: [],
authorized_actions: [],
name: 'Target 1',
host_sources: [],
@@ -0,0 +1,22 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupTest } from 'dummy/tests/helpers';
import Service from '@ember/service';
class SessionService extends Service {}
module('Unit | Service | indexed-db', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
this.owner.register('service:session', SessionService);
let service = this.owner.lookup('service:indexed-db');
assert.ok(service);
});
});
@@ -0,0 +1,45 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Transform | array', function (hooks) {
setupTest(hooks);
test('it serializes a principals array', function (assert) {
const transform = this.owner.lookup('transform:array');
const principal = [
{ id: 2, type: 'user' },
{ id: 3, type: 'group' },
{ id: 3, type: 'managed-group' },
];
const expectedArr = [
{ id: 2, type: 'user' },
{ id: 3, type: 'group' },
{ id: 3, type: 'managed-group' },
];
const emptyArr = [];
assert.deepEqual(transform.serialize(principal), expectedArr);
assert.deepEqual(transform.serialize(emptyArr), []);
});
test('it deserializes a principals array', function (assert) {
const transform = this.owner.lookup('transform:array');
const principal = [
{ id: 2, type: 'user' },
{ id: 3, type: 'group' },
{ id: 3, type: 'managed-group' },
];
const expectedArr = [
{ id: 2, type: 'user' },
{ id: 3, type: 'group' },
{ id: 3, type: 'managed-group' },
];
const emptyArr = [];
assert.deepEqual(transform.deserialize(principal), expectedArr);
assert.deepEqual(transform.deserialize(emptyArr), []);
});
});
@@ -1,17 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Transform | principal array', function (hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function (assert) {
let transform = this.owner.lookup('transform:principal-array');
assert.ok(transform);
});
});
@@ -0,0 +1,41 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { hashCode } from 'api/utils/hash-code';
import { module, test } from 'qunit';
module('Unit | Utility | hash-code', function () {
test('it hashes', function (assert) {
const result = hashCode({ a: 1, b: 2 });
assert.ok(result);
});
test('it hashes to same result regardless of object order', function (assert) {
const first = hashCode({ a: 1, b: 2, c: 3 });
const second = hashCode({ b: 2, c: 3, a: 1 });
assert.strictEqual(first, second);
});
test('it hashes to different result if objects are different', function (assert) {
const first = hashCode({ a: 1, b: 2, c: 3 });
const second = hashCode({ a: '1', b: '2', c: '3' });
assert.notStrictEqual(first, second);
});
test('it hashes an empty object', function (assert) {
const result = hashCode({});
assert.ok(result);
});
test('it hashes null to 0', function (assert) {
const result = hashCode(null);
assert.strictEqual(result, 0);
});
test('it hashes undefined to 0', function (assert) {
const result = hashCode(undefined);
assert.strictEqual(result, 0);
});
});
@@ -0,0 +1,532 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { queryIndexedDb } from 'api/utils/indexed-db-query';
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { pluralize } from 'ember-inflector';
import { camelize } from '@ember/string';
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
const seedIndexDb = async (resource, store, indexedDb, server) => {
const resourceData =
server.schema[pluralize(camelize(resource))].all().models;
const serializedData = resourceData.map((resource) =>
JSON.parse(JSON.stringify(server.serializerOrRegistry.serialize(resource))),
);
const serializer = store.serializerFor(resource);
const schema = store.modelFor(resource);
const normalizedPayload = serializer.normalizeResponse(
store,
schema,
{ items: serializedData },
null,
'query',
);
await indexedDb.db[resource].bulkPut(
normalizedPayload.data.map((datum) =>
indexedDb.normalizeData({
data: datum,
cleanData: true,
schema,
serializer,
}),
),
);
};
module('Unit | Utility | indexed-db-query', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let indexedDb;
let store;
let scope1;
let scope2;
const resource = 'target';
hooks.beforeEach(function () {
indexedDb = this.owner.lookup('service:indexed-db');
store = this.owner.lookup('service:store');
scope1 = this.server.create('scope', {
id: 'p_123',
type: 'project',
});
scope2 = this.server.create('scope', {
id: 'p_456',
type: 'project',
});
this.server.createList(resource, 5, {
type: TYPE_TARGET_SSH,
scopeId: scope1.id,
});
this.server.createList(resource, 5, {
type: TYPE_TARGET_TCP,
scopeId: scope2.id,
});
});
test('it filters by single field', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: [{ equals: TYPE_TARGET_SSH }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "and" logicalOperator', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'and',
values: [{ equals: TYPE_TARGET_SSH }],
},
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "or" logicalOperator', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'or',
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
},
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by multiple fields', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: [{ equals: TYPE_TARGET_SSH }],
scope_id: [{ equals: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by multiple fields with "or" logicalOperator', async function (assert) {
this.server.create(resource, { type: TYPE_TARGET_TCP, scopeId: scope1.id });
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'or',
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
},
scope_id: [{ equals: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 6);
});
test('it filters by multiple fields with "and" logicalOperator', async function (assert) {
this.server.create(resource, { type: TYPE_TARGET_TCP, scopeId: scope1.id });
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'and',
values: [{ equals: TYPE_TARGET_SSH }, { notEquals: TYPE_TARGET_TCP }],
},
scope_id: [{ equals: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by multiple fields and uses two "or" logicalOperators', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'or',
values: [{ equals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
},
scope_id: {
logicalOperator: 'or',
values: [{ equals: scope1.id }, { equals: scope2.id }],
},
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by multiple fields and uses two "and" logicalOperators', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: {
logicalOperator: 'and',
values: [{ notEquals: TYPE_TARGET_SSH }, { equals: TYPE_TARGET_TCP }],
},
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope2.id }, { notEquals: scope1.id }],
},
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it cannot filter with "contains" as initial operation', async function (assert) {
assert.expect(1);
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ contains: scope1.id }],
},
};
try {
await queryIndexedDb(indexedDb.db, resource, query);
} catch (e) {
assert.strictEqual(
e.message,
'Contains is not supported as the first filter.',
);
}
});
test('it cannot filter with "contains" as subsequent operation', async function (assert) {
assert.expect(2);
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope1.id }, { contains: scope2.id.slice(0, 2) }],
},
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
try {
await queryIndexedDb(indexedDb.db, resource, query);
} catch (e) {
assert.strictEqual(
e.message,
'Contains is not supported as a filter option with "or" operator.',
);
}
});
// TODO: Modify test cases that use comparison opertators to compare fields that
// are of type number. Additionally, add test case for searching on boolean fields.
test('it filters by single field with "gt" as initial operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ gt: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "gt" as subsequent operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope2.id }, { gt: scope1.id }],
},
},
};
let result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "gte" as initial operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ gte: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by single field with "gte" as subsequent operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope2.id }, { gte: scope1.id }],
},
},
};
let result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by single field with "lt" as initial operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ lt: scope2.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "lt" as subsequent operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope1.id }, { lt: scope2.id }],
},
},
};
let result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "lte" as initial operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ lte: scope2.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by single field with "lte" as subsequent operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope1.id }, { lte: scope2.id }],
},
},
};
let result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it filters by single field with "notEquals" as initial operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: [{ notEquals: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by single field with "notEquals" as subsequent operation', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
scope_id: {
logicalOperator: 'and',
values: [{ equals: scope2.id }, { notEquals: scope1.id }],
},
},
};
let result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
query.filters.scope_id.logicalOperator = 'or';
result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters and returns zero results', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
filters: {
type: [{ equals: 'unknown' }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 0);
});
test('it searches with a query that matches a field exactly', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = { search: scope1.id };
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it searches with a query that matches a field partially', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = { search: scope2.id.slice(0, 2) };
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 10);
});
test('it searches and returns no results with a query that matches no fields', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = { search: `${scope1.id}456789` };
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 0);
});
test('it filters by a single field and searches', async function (assert) {
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
search: scope1.id,
filters: {
type: [{ equals: TYPE_TARGET_SSH }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 5);
});
test('it filters by multiple fields and searches', async function (assert) {
const target = this.server.create(resource, {
name: 'Generated_ssh_target',
type: TYPE_TARGET_SSH,
scopeId: scope1.id,
});
await seedIndexDb(resource, store, indexedDb, this.server);
const query = {
search: target.name,
filters: {
type: [{ equals: TYPE_TARGET_SSH }],
scope_id: [{ equals: scope1.id }],
},
};
const result = await queryIndexedDb(indexedDb.db, resource, query);
assert.strictEqual(result.length, 1);
});
test('it filters on boolean type fields with truthy value', async function (assert) {
const authMethod = this.server.create('auth-method', {
name: 'Generated_auth_method',
scopeId: scope1.id,
});
scope1.update({ primaryAuthMethodId: authMethod.id });
await seedIndexDb('auth-method', store, indexedDb, this.server);
const query = { filters: { is_primary: [{ equals: 'true' }] } };
const result = await queryIndexedDb(indexedDb.db, 'auth-method', query);
assert.strictEqual(result.length, 1);
});
test('it filters on boolean type fields with falsy value', async function (assert) {
const authMethod = this.server.create('auth-method', {
name: 'Generated_auth_method',
scopeId: scope1.id,
});
scope1.update({ primaryAuthMethodId: authMethod.id });
await seedIndexDb('auth-method', store, indexedDb, this.server);
const query = { filters: { is_primary: [{ equals: 'false' }] } };
const result = await queryIndexedDb(indexedDb.db, 'auth-method', query);
assert.strictEqual(result.length, 0);
});
});
@@ -4,6 +4,7 @@
}}
<Hds::Dropdown
...attributes
@height='314px'
@onClose={{this.close}}
@listPosition={{@listPosition}}
@@ -11,7 +12,7 @@
>
<DD.ToggleButton
@color='secondary'
@text={{@name}}
@text={{@toggleText}}
@count={{@checkedItems.length}}
{{on 'click' this.open}}
/>
@@ -23,17 +23,19 @@ module('Integration | Component | dropdown/index', function (hooks) {
name: 'Fake Project',
displayName: 'Fake Project',
};
const name = 'Click Me';
const name = 'test';
const toggleText = 'Click Me';
const FIRST_ITEM_SELECTOR = `[data-test-checkbox="${firstItem.id}"]`;
const SECOND_ITEM_SELECTOR = `[data-test-checkbox="${secondItem.id}"]`;
const TOGGLE_DROPDOWN_SELECTOR = '.hds-dropdown-toggle-button';
const TOGGLE_DROPDOWN_SELECTOR = `[name=${name}] button`;
const SEARCH_INPUT_SELECTOR = '[data-test-dropdown-search]';
const COUNT_SELECTOR = '.hds-dropdown-toggle-button__count';
const ITEM_SELECTOR = '.hds-dropdown-list-item';
const COUNT_SELECTOR = `[name=${name}] button div:nth-child(2)`;
const ITEM_SELECTOR = 'li';
hooks.beforeEach(function () {
this.set('name', name);
this.set('toggleText', toggleText);
this.set('array', [firstItem, secondItem]);
this.set('checkedItems', [firstItem.id]);
this.set('applyFilter', () => {});
@@ -41,14 +43,15 @@ module('Integration | Component | dropdown/index', function (hooks) {
test('it renders', async function (assert) {
await render(hbs`<Dropdown
@name={{this.name}}
name={{this.name}}
@toggleText={{this.toggleText}}
@itemOptions={{this.array}}
@checkedItems={{this.checkedItems}}
@applyFilter={{this.applyFilter}}
/>`);
await click(TOGGLE_DROPDOWN_SELECTOR);
assert.dom('.hds-dropdown-toggle-button__text').hasText(name);
assert.dom(TOGGLE_DROPDOWN_SELECTOR).includesText(toggleText);
assert.dom(FIRST_ITEM_SELECTOR).isVisible();
assert.dom(FIRST_ITEM_SELECTOR).isChecked();
assert.dom(SECOND_ITEM_SELECTOR).isVisible();
@@ -58,7 +61,8 @@ module('Integration | Component | dropdown/index', function (hooks) {
test('it renders a search input when isSearchable is true', async function (assert) {
await render(hbs`<Dropdown
@name={{this.name}}
name={{this.name}}
@toggleText={{this.toggleText}}
@itemOptions={{this.array}}
@checkedItems={{this.checkedItems}}
@applyFilter={{this.applyFilter}}
@@ -71,7 +75,8 @@ module('Integration | Component | dropdown/index', function (hooks) {
test('it renders the correct count of checked items', async function (assert) {
await render(hbs`<Dropdown
@name={{this.name}}
name={{this.name}}
@toggleText={{this.toggleText}}
@itemOptions={{this.array}}
@checkedItems={{this.checkedItems}}
@applyFilter={{this.applyFilter}}
@@ -87,7 +92,8 @@ module('Integration | Component | dropdown/index', function (hooks) {
});
await render(hbs`<Dropdown
@name={{this.name}}
name={{this.name}}
@toggleText={{this.toggleText}}
@itemOptions={{this.array}}
@checkedItems={{this.checkedItems}}
@applyFilter={{this.applyFilter}}
@@ -102,7 +108,8 @@ module('Integration | Component | dropdown/index', function (hooks) {
this.set('checkedItems', []);
await render(hbs`<Dropdown
@name={{this.name}}
name={{this.name}}
@toggleText={{this.toggleText}}
@itemOptions={{this.array}}
@checkedItems={{this.checkedItems}}
@applyFilter={{this.applyFilter}}
@@ -128,7 +135,8 @@ module('Integration | Component | dropdown/index', function (hooks) {
);
await render(hbs`<Dropdown
@name={{this.name}}
name={{this.name}}
@toggleText={{this.toggleText}}
@itemOptions={{this.itemOptions}}
@checkedItems={{this.checkedItems}}
@applyFilter={{this.applyFilter}}
@@ -44,6 +44,7 @@ toggle-secret: Toggle secret visibility
minimize: Minimize
fullscreen: Fullscreen
clear-all-filters: Clear all filters
clear: Clear
clear-filters: Clear filters
learn-more: Learn more
read-more: Read more
+2
View File
@@ -41,6 +41,7 @@ titles:
no-results-found: No results found
filters-applied: 'Filters Applied:'
unit: Unit
not-set: Not set
year: '{numberOfYears, plural, =1 {# year} other {# years}}'
days: '{days, plural, =1 {# day} other {# days}}'
descriptions:
@@ -59,6 +60,7 @@ questions:
remove-confirm: Are you sure you want to remove this association?
abandon-confirm: Are you sure you want to navigate away? Your unsaved changes will be lost.
retry-confirm: Would you like to retry?
clear-confirm: Are you sure you want to clear the resource id?
themes:
system: System Default
light: Light
+1 -1
View File
@@ -168,7 +168,7 @@ ip_addresses:
help: Read-only public and private IP addresses given by the plugin host catalog provider
sync-interval:
label: Sync Interval
help: Optional. Number of seconds between the time Boundary syncs hosts in this set.
help: Number of seconds between the time Boundary syncs hosts in this set.
tags:
label: Tags
secret-editor:
@@ -7,3 +7,4 @@ delete-success: Deleted successfully.
add-success: Added successfully.
remove-success: Removed successfully.
canceled-success: Canceled successfully.
clear-success: Cleared successfully
+53 -12
View File
@@ -62,6 +62,7 @@ auth-method:
searching: Searching
finding: Finding
always: Always
primary: Primary
types:
password: Password
oidc: OIDC
@@ -83,7 +84,7 @@ auth-method:
form:
urls:
label: Server address (URL and port)
help: 'The domain name or IP address of the LDAP server. (Example: ldap.example.com:389)'
help: 'The domain name or IP address of the LDAP server. (Example: ldaps://ldap.example.com:636)'
certificates:
label: Certificates
help: PEM encoded x509 certificates in ASN.1 DER form that can be used as trust anchors when connecting to an LDAP provider.
@@ -418,11 +419,12 @@ storage-bucket:
messages:
none:
title: No storage buckets yet
description: To start using session recording, you need to have an Amazon S3 bucket.
description: To start using session recording, you need to have a storage bucket.
link: Add a new storage bucket
titles:
new: New Storage Bucket
amazon_s3: Amazon S3
minio: MinIO
actions:
delete: Delete Storage Bucket
types:
@@ -434,6 +436,9 @@ storage-bucket:
dynamic:
title: Dynamic
description: Specify an Assume Role used to generate credentials to use with this AWS bucket
plugin-types:
aws: Amazon S3
minio: MinIO
form:
scope:
label: Scope
@@ -443,33 +448,36 @@ storage-bucket:
provider:
label: Provider
help: Boundary currently only supports Amazon S3 storage.
endpoint_url:
label: Endpoint URL
help: Cannot be changed later.
bucket_name:
label: Bucket name
help: The name of this bucket in AWS
help: The name of this storage bucket.
bucket_prefix:
label: Bucket prefix
help: The base path where session recordings will be stored.
role_arn:
label: Role ARN
help: The ARN (Assume Role Name) used by this instance that the worker runs on
help: The ARN (Assume Role Name) used by this instance that the worker runs on.
role_external_id:
label: Role external ID
help: Used to provide access to this account if you have this configured
help: Used to provide access to this account if you have this configured.
role_tags:
label: Role tags
help: Attributes to pass to AWS
help: Attributes to pass to AWS.
role_session_name:
label: Role session name
help: Unique identifier for the AWS session to help identify it in your logs
help: Unique identifier for the AWS session to help identify it in your logs.
region:
label: Region
help: The AWS region to use
help: The region that this storage bucket is in.
access_key_id:
label: Access key ID
help: The Access key ID generated by AWS for the IAM User to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
help: The Access key ID generated by your storage provider for the IAM User to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
secret_access_key:
label: Secret access key
help: The secret access key generated by AWS for the IAM user to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
help: The secret access key generated by your storage provider for the IAM user to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
worker_filter:
label: Worker filter
help: Filter for Boundary workers that have network access to your storage. Boolean expression.
@@ -576,6 +584,7 @@ target:
new: New Target
active-sessions: Active sessions
sessions-flyout: Active sessions for {targetDisplayName}
aliases-flyout: Aliases for {targetDisplayName}
actions:
create: New Target
delete: Delete Target
@@ -586,6 +595,8 @@ target:
enable-session-recording: Enable Session Recording
enable-recording: Enable recording
view-more-sessions: View more sessions
add-an-alias: Add an alias
view-more-aliases: View more aliases
types:
tcp: Generic TCP
ssh: SSH
@@ -646,8 +657,8 @@ target:
filter:
label: Record sessions for this target
select:
label: AWS storage buckets
description: Sessions will be recorded and stored in this location. Must use Amazon S3.
label: Storage buckets
description: Sessions will be recorded and stored in this location.
brokered-credential-source:
title: Brokered Credential
title_plural: Brokered Credentials
@@ -1002,5 +1013,35 @@ policy:
alias:
title: Alias
title_plural: Aliases
description: An Alias is a DNS-like string associated with a destination resource.
details: Alias Details
titles:
new: New Alias
resource_id: Resource ID
points_to: Points to
messages:
none:
title: Welcome to Aliases
description: No Aliases yet
link: Create a new alias
delete: Deleting this alias may break any workflows using it. You can also clear the alias of the resource it points to without deleting it and then edit it to point it to a new resource.
form:
options:
label: Alias options
type:
label: Target
help: Servers, API Endpoints, and network devices
destination_id:
label: Target ID
help: Specify any target across your deployment to associate this alias with
value:
label: Alias Value
help: The alias you would like to use for this resource
authorize_session_arguments:
label: Host ID
help: Always use this host when connecting using this alias
sidebar:
help: Use a domain name as a shortcut for anyone with access to this target.
alias:
label: Aliases
help: You can create aliases for this target that will make it easier to reach. Users must have access to this project to use aliases.
@@ -17,3 +17,4 @@ failed: Failed
loading:
title: Loading all items...
description: All of your {resource} are still loading. Please wait while we finish loading everything on screen.
generic-description: All of your resources are still loading. Please wait while we finish loading everything on screen.
@@ -1,37 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<article ...attributes class='rose-card-link'>
<LinkTo
title={{@title}}
@route={{if @route @route 'index'}}
@model={{@model}}
>
<div class='rose-card-link-header'>
{{#if @title}}
<h2 class='rose-card-link-title'>
{{#if @icon}}
<Rose::Icon @name={{@icon}} @size='medium' />
{{/if}}
<span class='rose-card-link-title-text'>{{@title}}</span>
</h2>
{{/if}}
</div>
<div class='rose-card-link-body'>
{{#if @description}}
<p class='rose-card-link-description'>{{@description}}</p>
{{/if}}
</div>
<div class='rose-card-link-footer'>
{{#if @id}}
<p class='rose-card-link-footer-id'>{{@id}}</p>
{{/if}}
</div>
</LinkTo>
{{yield}}
</article>
@@ -1,8 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<div ...attributes class='rose-cards'>
{{yield (hash link=(component 'rose/card/link'))}}
</div>
@@ -31,6 +31,13 @@
color: var(--token-form-indicator-optional-color);
}
// Override for sortable HDS table columns that ignore selected theme in app
// and use the system default theme causing color mismatch if selected theme
// is not the system default theme
.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
@@ -1,66 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | rose/card/link', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
await render(hbs`<Rose::Card::Link />`);
assert.ok(find('article'));
assert.ok(find('.rose-card-link'));
assert.ok(find('.rose-card-link-header'));
assert.ok(find('.rose-card-link-body'));
assert.ok(find('.rose-card-link-footer'));
});
test('it renders with @title', async function (assert) {
await render(hbs`<Rose::Card::Link @title="card title" />`);
assert.strictEqual(find('a').title, 'card title');
assert.strictEqual(
find('.rose-card-link-title').textContent.trim(),
'card title',
);
});
test('it renders with @description', async function (assert) {
await render(hbs`<Rose::Card::Link @description="card description" />`);
assert.strictEqual(
find('.rose-card-link-body').textContent.trim(),
'card description',
);
});
test('it renders with @id', async function (assert) {
await render(hbs`<Rose::Card::Link @id="card_id" />`);
assert.strictEqual(
find('.rose-card-link-footer-id').textContent.trim(),
'card_id',
);
});
test('it renders with @icon', async function (assert) {
await render(
hbs`<Rose::Card::Link @title="foo" @icon="flight-icons/svg/alert-circle-16" />`,
);
assert.ok(find('.rose-icon'));
});
test('it renders with attributes', async function (assert) {
await render(hbs`<Rose::Card::Link id="card" />`);
assert.ok(find('#card'));
});
test('it renders with content', async function (assert) {
await render(hbs`<Rose::Card::Link>
<button type="button" />
</Rose::Card::Link>`);
assert.ok(find('button'));
});
});
@@ -1,26 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | rose/cards', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
await render(hbs`
<Rose::Cards as |cards|>
<cards.link
@title="Card Title"
@description="Description"
/>
</Rose::Cards>
`);
assert.ok(find('.rose-cards'));
assert.ok(find('.rose-card-link'));
});
});
@@ -23,7 +23,7 @@
<form.radioGroup
@name='theme'
@selectedValue={{@model.state}}
@changed={{route-action 'changeState'}}
@changed={{fn @changeState @model}}
as |radioGroup|
>
{{#each @model.options.state as |state|}}
@@ -48,14 +48,14 @@
{{#if @model.isPrimary}}
<dropdown.button
@disabled={{@model.canSave}}
{{on 'click' (route-action 'removeAsPrimary' @model)}}
{{on 'click' (fn @removeAsPrimary @model)}}
>
{{t 'resources.auth-method.actions.remove-as-primary'}}
</dropdown.button>
{{else}}
<dropdown.button
@disabled={{@model.canSave}}
{{on 'click' (route-action 'makePrimary' @model)}}
{{on 'click' (fn @makePrimary @model)}}
>
{{t 'resources.auth-method.actions.make-primary'}}
</dropdown.button>
@@ -86,7 +86,7 @@
<dropdown.button
@style='danger'
@disabled={{@model.canSave}}
{{on 'click' (route-action 'delete' @model)}}
{{on 'click' (fn @delete @model)}}
>
{{t 'resources.auth-method.actions.delete'}}
</dropdown.button>
@@ -76,11 +76,11 @@
<Hds::Form::TextInput::Field
name='password'
@type='password'
@value={{@model.password}}
@value={{this.password}}
@isInvalid={{@model.errors.password}}
disabled={{form.disabled}}
autocomplete='new-password'
{{on 'input' (set-from-event @model 'password')}}
{{on 'input' (set-from-event this 'password')}}
as |F|
>
<F.Label>{{t 'form.password.label'}}</F.Label>
@@ -0,0 +1,153 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<Hds::Form::TextInput::Field
name='name'
@isOptional={{true}}
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
{{#if @model.errors.name}}
<F.Error as |E|>
{{#each @model.errors.name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
name='description'
@isOptional={{true}}
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
<Hds::Form::Fieldset as |F|>
<F.Legend>
{{t 'resources.alias.form.options.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.alias.description'}}
</F.HelperText>
<F.Control>
<Hds::Form::RadioCard::Group @name='type' @alignment='center' as |G|>
<G.Legend>{{t 'resources.target.form.type.label'}}</G.Legend>
<G.RadioCard @maxWidth='15rem' @checked='true' @disabled='true' as |R|>
<R.Label>{{t 'resources.alias.form.type.label'}}</R.Label>
<R.Description>{{t 'resources.alias.form.type.help'}}</R.Description>
<R.Icon @name='target' />
</G.RadioCard>
</Hds::Form::RadioCard::Group>
</F.Control>
</Hds::Form::Fieldset>
<Hds::Form::TextInput::Field
name='value'
@isRequired={{true}}
@value={{@model.value}}
@isInvalid={{@model.errors.value}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'value')}}
as |F|
>
<F.Label>{{t 'resources.alias.form.value.label'}}</F.Label>
<F.HelperText>{{t 'resources.alias.form.value.help'}}</F.HelperText>
{{#if @model.errors.value}}
<F.Error as |E|>
{{#each @model.errors.value as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::TextInput::Field
name='destination_id'
@isOptional={{true}}
@value={{@model.destination_id}}
@isInvalid={{@model.errors.destination_id}}
readonly={{@hideTargetId}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'destination_id')}}
as |F|
>
<F.Label>{{t 'resources.alias.form.destination_id.label'}}</F.Label>
<F.HelperText>{{t
'resources.alias.form.destination_id.help'
}}</F.HelperText>
{{#if @model.errors.destination_id}}
<F.Error as |E|>
{{#each @model.errors.destination_id as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::TextInput::Field
name='authorize_session_arguments'
@isOptional={{true}}
@value={{@model.authorize_session_arguments.host_id}}
@isInvalid={{@model.errors.authorize_session_arguments.host_id}}
disabled={{form.disabled}}
{{on 'input' this.handleHostIdChange}}
as |F|
>
<F.Label>{{t
'resources.alias.form.authorize_session_arguments.label'
}}</F.Label>
<F.HelperText>{{t
'resources.alias.form.authorize_session_arguments.help'
}}</F.HelperText>
{{#if @model.errors.authorize_session_arguments.host_id}}
<F.Error as |E|>
{{#each @model.errors.authorize_session_arguments.host_id as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<InfoField
@value={{@model.scopeModel.displayName}}
@icon='globe'
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'resources.scope.title'}}</F.Label>
</InfoField>
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -0,0 +1,20 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class FormAliasComponent extends Component {
//actions
/**
* Handles input changes
*/
@action
handleHostIdChange({ target: { value } }) {
this.args.model.authorize_session_arguments = {
host_id: value,
};
}
}
@@ -97,8 +97,8 @@
<Hds::Form::TextInput::Field
name='client_id'
@value={{@model.client_id}}
@isRequired={{true}}
@isInvalid={{@model.errors.client_id}}
@isOptional={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'client_id')}}
as |F|
@@ -117,8 +117,8 @@
<Hds::Form::TextInput::Field
name='client_secret'
@value={{@model.client_secret}}
@isRequired={{true}}
@isInvalid={{@model.errors.client_secret}}
@isOptional={{true}}
disabled={{form.disabled}}
@hasVisibilityToggle={{false}}
@type='password'
@@ -147,7 +147,11 @@
{{/unless}}
{{! Signing Algorithms }}
<Form::Field::ListWrapper @layout='horizontal' @disabled={{form.disabled}}>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'form.signing_algorithms.label'}}
@@ -412,6 +416,7 @@
<Hds::Form::TextInput::Field
name='max_age'
@value={{@model.max_age}}
@isOptional={{true}}
@isInvalid={{@model.errors.max_age}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'max_age')}}
@@ -431,6 +436,7 @@
<Hds::Form::TextInput::Field
name='api_url_prefix'
@value={{@model.api_url_prefix}}
@isRequired={{true}}
@isInvalid={{@model.errors.api_url_prefix}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'api_url_prefix')}}
@@ -11,11 +11,10 @@
as |form|
>
<Hds::Form::TextInput::Field
@isOptional={{true}}
name='name'
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
@type='text'
name='name'
@isOptional={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
@@ -25,17 +24,19 @@
{{#if @model.errors.name}}
<F.Error as |E|>
{{#each @model.errors.name as |error|}}
<E.Message>{{error.message}}</E.Message>
<E.Message>
{{error.message}}
</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
@isOptional={{true}}
name='description'
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
name='description'
@isOptional={{true}}
disabled={{form.disabled}}
as |F|
>
@@ -11,12 +11,12 @@
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<Hds::Form::TextInput::Field
@isOptional={{true}}
name='name'
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
@type='text'
name='name'
@isOptional={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
@@ -26,17 +26,19 @@
{{#if @model.errors.name}}
<F.Error as |E|>
{{#each @model.errors.name as |error|}}
<E.Message>{{error.message}}</E.Message>
<E.Message>
{{error.message}}
</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
@isOptional={{true}}
name='description'
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
name='description'
@isOptional={{true}}
disabled={{form.disabled}}
as |F|
>
@@ -4,7 +4,7 @@
*/
import Component from '@glimmer/component';
import { types } from 'api/models/credential-store';
import { TYPES_CREDENTIAL_STORE } from 'api/models/credential-store';
//Note: this is a temporary solution till we have resource type helper in place
const icons = ['keychain', 'vault'];
@@ -16,6 +16,9 @@ export default class FormStaticCredentialStoreIndexComponent extends Component {
* @type {object}
*/
get mapResourceTypeWithIcon() {
return types.reduce((obj, type, i) => ({ ...obj, [type]: icons[i] }), {});
return TYPES_CREDENTIAL_STORE.reduce(
(obj, type, i) => ({ ...obj, [type]: icons[i] }),
{},
);
}
}
@@ -116,6 +116,7 @@
<Hds::Form::TextInput::Field
name='worker_filter'
@value={{@model.worker_filter}}
@isOptional={{true}}
@isInvalid={{@model.errors.worker_filter}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'worker_filter')}}
@@ -178,6 +179,7 @@
<Hds::Form::TextInput::Field
name='namespace'
@value={{@model.namespace}}
@isOptional={{true}}
@isInvalid={{@model.errors.namespace}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'namespace')}}
@@ -199,6 +201,7 @@
<Hds::Form::TextInput::Field
name='tls_server_name'
@value={{@model.tls_server_name}}
@isOptional={{true}}
@isInvalid={{@model.errors.tls_server_name}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'tls_server_name')}}
@@ -4,7 +4,7 @@
*/
import Component from '@glimmer/component';
import { types } from 'api/models/credential-store';
import { TYPES_CREDENTIAL_STORE } from 'api/models/credential-store';
//Note: this is a temporary solution till we have resource type helper in place
const icons = ['keychain', 'vault'];
@@ -16,6 +16,9 @@ export default class FormVaultCredentialStoreIndexComponent extends Component {
* @type {object}
*/
get mapResourceTypeWithIcon() {
return types.reduce((obj, type, i) => ({ ...obj, [type]: icons[i] }), {});
return TYPES_CREDENTIAL_STORE.reduce(
(obj, type, i) => ({ ...obj, [type]: icons[i] }),
{},
);
}
}
@@ -51,6 +51,7 @@
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
{{#if @model.isNew}}
<Hds::Form::RadioCard::Group @name='type' as |G|>
<G.Legend>{{t 'form.type.label'}}</G.Legend>
@@ -107,6 +107,7 @@
<Hds::Form::TextInput::Field
name='region'
@value={{@model.region}}
@isRequired={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'region')}}
as |F|
@@ -123,6 +124,7 @@
<Hds::Form::TextInput::Field
name='access_key_id'
@value={{@model.access_key_id}}
@isRequired={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'access_key_id')}}
as |F|
@@ -139,6 +141,7 @@
<Hds::Form::TextInput::Field
name='secret_access_key'
@value={{@model.secret_access_key}}
@isRequired={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'secret_access_key')}}
as |F|
@@ -5,20 +5,23 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { types, pluginTypes } from 'api/models/host-catalog';
import {
TYPES_HOST_CATALOG,
TYPES_HOST_CATALOG_PLUGIN,
} from 'api/models/host-catalog';
//Note: this is a temporary solution till we have resource type helper in place
const icons = ['aws-color', 'azure-color'];
export default class FormHostCatalogAwsComponent extends Component {
// =properties
hostCatalogTypes = types;
hostCatalogTypes = TYPES_HOST_CATALOG;
/**
* maps resource type with icon
* @type {object}
*/
get mapResourceTypeWithIcon() {
return pluginTypes.reduce(
return TYPES_HOST_CATALOG_PLUGIN.reduce(
(obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }),
{},
);
@@ -107,6 +107,7 @@
<Hds::Form::TextInput::Field
name='tenant_id'
@value={{@model.tenant_id}}
@isRequired={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'tenant_id')}}
as |F|
@@ -123,6 +124,7 @@
<Hds::Form::TextInput::Field
name='subscription_id'
@value={{@model.subscription_id}}
@isRequired={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'subscription_id')}}
as |F|
@@ -139,6 +141,7 @@
<Hds::Form::TextInput::Field
name='client_id'
@value={{@model.client_id}}
@isRequired={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'client_id')}}
as |F|
@@ -155,6 +158,7 @@
<Hds::Form::TextInput::Field
name='secret_value'
@value={{@model.secret_value}}
@isRequired={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'secret_value')}}
as |F|
@@ -4,20 +4,23 @@
*/
import Component from '@glimmer/component';
import { types, pluginTypes } from 'api/models/host-catalog';
import {
TYPES_HOST_CATALOG,
TYPES_HOST_CATALOG_PLUGIN,
} from 'api/models/host-catalog';
//Note: this is a temporary solution till we have resource type helper in place
const icons = ['aws-color', 'azure-color'];
export default class FormAzureHostCatalogAwsComponent extends Component {
// =properties
hostCatalogTypes = types;
hostCatalogTypes = TYPES_HOST_CATALOG;
/**
* maps resource type with icon
* @type {object}
*/
get mapResourceTypeWithIcon() {
return pluginTypes.reduce(
return TYPES_HOST_CATALOG_PLUGIN.reduce(
(obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }),
{},
);
@@ -4,20 +4,23 @@
*/
import Component from '@glimmer/component';
import { types, pluginTypes } from 'api/models/host-catalog';
import {
TYPES_HOST_CATALOG,
TYPES_HOST_CATALOG_PLUGIN,
} from 'api/models/host-catalog';
//Note: this is a temporary solution till we have resource type helper in place
const icons = ['aws-color', 'azure-color'];
export default class FormStaticHostCatalogAwsComponent extends Component {
// =properties
hostCatalogTypes = types;
hostCatalogTypes = TYPES_HOST_CATALOG;
/**
* maps resource type with icon
* @type {object}
*/
get mapResourceTypeWithIcon() {
return pluginTypes.reduce(
return TYPES_HOST_CATALOG_PLUGIN.reduce(
(obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }),
{},
);
@@ -97,7 +97,11 @@
</:field>
</Form::Field::ListWrapper>
<Form::Field::ListWrapper @layout='horizontal' @disabled={{form.disabled}}>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.host-set.form.filter.label'}}
@@ -127,13 +131,14 @@
name='sync_interval_seconds'
@value={{@model.sync_interval_seconds}}
@isInvalid={{@model.errors.sync_interval_seconds}}
@isOptional={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'sync_interval_seconds')}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.Label>{{t 'form.sync-interval.label'}}</F.Label>
<F.HelperText>
{{t 'form.name.help'}}
{{t 'form.sync-interval.help'}}
<Hds::Link::Inline @href={{doc-url 'host-set.sync-interval-seconds'}}>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
@@ -123,13 +123,14 @@
name='sync_interval_seconds'
@value={{@model.sync_interval_seconds}}
@isInvalid={{@model.errors.sync_interval_seconds}}
@isOptional={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'sync_interval_seconds')}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.Label>{{t 'form.sync-interval.label'}}</F.Label>
<F.HelperText>
{{t 'form.name.help'}}
{{t 'form.sync-interval.help'}}
<Hds::Link::Inline @href={{doc-url 'host-set.sync-interval-seconds'}}>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
@@ -35,7 +35,7 @@ export default class FormRoleAddPrincipalsIndexComponent extends Component {
get filteredPrincipals() {
// Retrieve principal IDs assigned to current role
const currentPrincipalIDs = this.args.model.principals.map(
(principal) => principal.principal_id,
(principal) => principal.id,
);
// Retrieve principal IDs not assigned to current role
Loaded 100 of 356 files, more files were not shown because too many files have changed in this diff. Show more