Compare commits

...
90 Commits
Author SHA1 Message Date
Zhihe Li dde2f259e8 chore: 🤖 Update changelog (#2516) 2024-10-11 11:58:16 -04:00
Zhihe Li e38c28c288 fix: 🐛 Push associated targets in sessions to ember store (#2518) 2024-10-10 17:03:27 -04:00
Dharini Jeeva 0ff91f9182 fix: 🐛 minor UI changes, ent flag for worker filter (#2517) 2024-10-10 12:44:12 -07:00
Lisbet Alvarez 857bf01596 style: 💄 issues with spacing in manage scopes pages for a role (#2515)
* style: 💄 issues with spacing in manage scopes pages for a role

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15287
2024-10-09 15:17:42 -07:00
Dharini Jeeva 965e82473d Types for aws host catalogs (#2514)
assume role dynamic AWS credentials
2024-10-09 14:49:22 -07:00
Dharini Jeeva 0d55812f64 Add worker type dynamic host catalog (#2511)
* fix: 🐛 add worker filter support for aws host catalogs
2024-10-08 10:58:52 -07:00
Lisbet Alvarez e807e1a3af fix: 🐛 cannot retry oidc authentication in desktop & admin (#2512)
* fix: 🐛 cannot retry oidc authentication in desktop & admin

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15250
2024-10-08 10:29:21 -07:00
Lisbet Alvarez 64e568bd72 fix: 🐛 deleting session recording causes error (#2510)
* fix: 🐛 deleting session recording causes error

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

* fix: 🐛 remove scope filter & remove unused translation

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15236
2024-10-04 08:14:30 -07:00
Carlos Alcaide CorvoandDharini Jeeva 401ff354e8 Llb ts desktop no cli (#2491)
Changes to allow Desktop client build with no CLI
---------
Co-authored-by: Dharini Jeeva <dharini@hashicorp.com>
2024-10-04 16:35:18 +02:00
Michael Li 9c31154941 test(e2e): Updates from 0.18.0 testing (#2509)
* test(e2e): Update locators that can't be found

* test(e2e): Fix race condition

It seems like the alias tests were failing as it was unable to open the Manage menu here. One theory is that we need to make sure the alias page has fully loaded before it tries to click the Manage button (the test might be finding the Manage button from the previous screen and clicking that)

* CR: Fix Alias header check
2024-10-04 08:39:20 -04:00
Lisbet Alvarez 1947365c91 chore: 🤖 resolve rollup vulnerability by removing ember-fetch (#2505)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15189
2024-10-02 16:11:22 -07:00
Michael Li be528dae47 test(e2e): Add test for worker tags (#2507) 2024-10-02 14:04:38 -04:00
Cameron Perera d3c789c66c Icu 15181 add pagination to session recordings (#2502)
* refactor: 💡 move model hook to session-recordings/index route

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

* test: 💍 add missing tests for session recordings controller

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

* feat: 🎸 paginate session-recordings and search

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

* refactor: 💡 fix failing redirect and tests

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

* test: 💍 add search tests for session recordings

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

* feat: 🎸 add user filtering for session recordings

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

* feat: 🎸 add more attributes to indexed-db session recordings

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

* feat: 🎸 add target filter for session recordings

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

* refactor: 💡 update filterOptions method

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

* feat: 🎸 add scopes filtering to session recording

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

* feat: 🎸 add time filter to session recordings

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

* refactor: 💡 add no filter results text

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

* refactor: 💡 update Exist variables to doExist

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

* refactor: 💡 updates from PR feedback

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

* refactor: 💡 remove scope_id filter

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

* refactor: 💡 change session recording placeholder text

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

* chore: 🤖 increment indexedDB version

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

* test: 💍 add Date object tests for indexed-db

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

* refactor: 💡 small changes from PR feedback

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

* refactor: 💡 change times filter array to time date object

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

* test: 💍 add more date specific indexed-db test

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

* refactor: 💡 change Today time shortcut to "Last 24 hours"

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

* test: 💍 fix date mock in session-recording controller tests

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15181
2024-10-01 13:55:15 -05:00
Michael Li 15727f61fd test(e2e): Add Admin UI test for vault credential libraries (enterprise) (#2501) 2024-10-01 12:11:14 -04:00
hashicorp-tsccr[bot]andhashicorp-tsccr[bot] 486d8b116a Result of tsccr-helper -log-level=info gha update -latest . (#2506)
Co-authored-by: hashicorp-tsccr[bot] <hashicorp-tsccr[bot]@users.noreply.github.com>
2024-10-01 14:32:36 +02:00
91ffbfb315 icu-14735 add search & pagination to aliases (#2498)
* refactor: 💡 add search & pagination to aliases

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

Co-authored-by: Dharini Jeeva <dharini@hashicorp.com>
Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
2024-09-30 13:07:13 -07:00
Dharini Jeeva 9b33812f9e fix: 🐛 bucket types icon in target association (#2503)
* fix: 🐛 bucket types icon in target association
2024-09-27 12:23:16 -07:00
Zhihe Li b40c53fb45 feat: 🎸 Update alert designs (#2496) 2024-09-24 14:30:52 -04:00
Zhihe Li 97258f7525 Use updated cache daemon (#2475)
* feat: 🎸 Handle truncated cache daemon response
2024-09-24 14:30:52 -04:00
Zhihe Li ace65eab3a fix: 🐛 Fix theme selector not properly selecting option (#2495) 2024-09-24 11:58:26 -04:00
Zhihe Li 20e6df522a feat: 🎸 Hide daemon errors initially (#2477)
* feat: 🎸 Hide daemon errors initially

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

* feat: 🎸 Move the user and server cards up in settings

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14962
2024-09-24 11:58:26 -04:00
Dharini Jeeva c8abc3845a fix: 🐛 pass current usl to maintain session (#2472)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14963
2024-09-24 11:58:26 -04:00
Zhihe Li 6cd2f0f7d8 fix: 🐛 Fix windows hanging when client agent isn't running (#2459) 2024-09-24 11:58:25 -04:00
Zhihe Li 1266930647 chore: 🤖 Remove client agent feature flag (#2449) 2024-09-24 11:58:25 -04:00
Dharini Jeeva 695d9d413d fix: 🐛 remove connected lable (#2446) 2024-09-24 11:58:25 -04:00
Zhihe Li edaf19f59b feat: 🎸 Add section for client agent in settings (#2442)
* feat: 🎸 Add section for client agent in settings

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14780
2024-09-24 11:58:25 -04:00
Dharini Jeeva de5defeab2 Icu 14781 add a section for server information (#2438) 2024-09-24 11:58:25 -04:00
Zhihe Li 63ec418cf0 feat: 🎸 Show cache daemon errors from status (#2437)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14816
2024-09-24 11:58:25 -04:00
Zhihe Li e4f4394197 Icu 14403 add dropdown to select and save log verbosity (#2431)
* feat: 🎸 Add logging settings card

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14403
2024-09-24 11:58:25 -04:00
Dharini Jeeva 0b9357e9b3 chore: 🤖 add user info (#2425)
* chore: 🤖 add user info
2024-09-24 11:58:25 -04:00
Dharini Jeeva 777380e19f Add theme selection (#2428)
* fix: 🐛 remove theme from main menu bar and move to settings pag

* chore: 🤖 move toggletheme to app controller
2024-09-24 11:58:24 -04:00
Dharini Jeeva 25b3a940a3 Icu 14400 add a section for version info cli and dc (#2409)
* chore: 🤖 cli info
2024-09-24 11:58:24 -04:00
Dharini Jeeva a97f66e011 feat: 🎸 settings page (#2395)
* feat: 🎸 settings page
2024-09-24 11:58:24 -04:00
Cameron Perera fac7d2b128 small update to desktop README (#2497) 2024-09-23 16:05:10 -05:00
Dharini Jeeva ad1bb34453 fix: show username first and then password when credentials are listed (#2494) 2024-09-19 14:22:25 -07:00
Lisbet Alvarez 8605a54221 refactor: 💡 remove route-action usage for scopes in desktop (#2447)
* refactor: 💡 remove route-action usage for scopes in desktop

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12621
2024-09-17 16:36:47 -07:00
Lisbet Alvarez 98cd9dc1ec chore: 🤖 add resolution for send (#2490) 2024-09-16 12:27:13 -07:00
Cameron Perera bd857836ee Icu 14208 vulnerabilities in boundary UI enterprise lodash template 4 5 0 in yarn lock (#2485)
* fix: 🐛 add patch for lodash.template

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

* chore: 🤖 add patch-package and postinstall-postinstall

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

* fix: 🐛 add patch for lodash.template

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

* chore: 🤖 add patch-package and postinstall-postinstall

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14208
2024-09-16 09:27:40 -05:00
Carlos Alcaide Corvo 4974273bd0 chore: 🤖 Upgrade git-cz (#2488)
Upgrade git-cz from 4.8.0 to 4.9.0. As per git-cz changelog, there are
no breaking changes
2024-09-13 16:14:21 -07:00
Lisbet Alvarez c4e58fb780 refactor: 💡 change channel app state title & add alert msg (#2489)
* refactor: 💡 change channel app state title & add alert msg

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-11148
2024-09-13 13:24:45 -07:00
dependabot[bot]andCameron Perera 2515efead6 chore(deps-dev): bump webpack from 5.88.2 to 5.94.0 (#2466)
Bumps [webpack](https://github.com/webpack/webpack) from 5.88.2 to 5.94.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.88.2...v5.94.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

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-09-13 14:12:58 -05:00
Cameron Perera fda3ca9d4b fix: 🐛 add regex literal for isLocalHost check (#2484)
* fix: 🐛 add ':' to isLocalHost check

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

* refactor: 💡 use regex to verify href is localhost

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13242
2024-09-13 13:03:40 -05:00
Cameron Pereraandcameronperera 9648e1fa3b Add duration unit format (#2483)
* chore: 🤖 add @formatjs/intl-durationformat to addons/core

* refactor: 💡 replace old duration format with new package

* chore: 🤖 remove intl-unofficial-duration-unit-format

* Add missing copyright headers

* test: 💍 add more test cases

---------

Co-authored-by: cameronperera <cameronperera@users.noreply.github.com>
2024-09-13 09:43:08 -05:00
Lisbet Alvarez e395b1196c Chore resolve path to regexp vulnerability (#2482)
* chore: 🤖 resolve path-toregexp vulnerability

* chore: 🤖 resolve body-parser vulnerability
2024-09-11 14:20:47 -07:00
Lisbet Alvarez b7e6492d26 chore: 🤖 add resolution for micromatch (#2481) 2024-09-10 09:28:47 -07:00
Zhihe Li 6311d7587e fix: 🐛 Fix daemon getting killed if started before DC (#2480) 2024-09-09 14:55:38 -04:00
Cameron Perera 07573d6b2c Fix/json5 security issue (#2479)
* chore: 🤖 delete and re-install find-babel-config from yarn.lock

* chore: 🤖 remove json5 resolution
2024-09-06 14:45:38 -05:00
Lisbet Alvarez 90890441e1 chore: 🤖 upgrade braces to 3.0.3 to fix security vulnerability (#2476)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14205
2024-09-06 10:32:30 -07:00
Cameron Perera 1430adce7e refactor: 💡 update test selectors for storage-buckets list (#2463)
* refactor: 💡 update test selectors for storage-buckets list

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

* refactor: 💡 update sidebar nav link test selector

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14034
2024-09-06 09:56:24 -05:00
Cameron Perera 89afee7297 chore: 🤖 upgrade semver versions (#2473)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14213
2024-09-06 09:33:57 -05:00
Zhihe Li 78f89da348 style: 💄 Fix text description (#2478) 2024-09-05 17:53:19 -04:00
Cameron Perera 9bacb55e87 chore: 🤖 update TODO to with ICU ticket (#2474)
* chore: 🤖 update TODO to with ICU ticket

* chore: 🤖 add message back to todo
2024-09-05 10:21:17 -05:00
Cameron Perera dea4a62955 test: 💍 add TODO for a11y violations (#2471) 2024-09-04 15:44:42 -05:00
Lisbet Alvarez ef163709f2 Upgrade ember a11y testing to v7 (#2470)
* chore: 🤖 update ember-a11y-testing and @ember/test-helpers

* chore: 🤖 upgrade to v7

* chore: 🤖 upgrade deps and force resolutions to fix errors

* chore: 🤖 fix ember-a11y errors due to summary title not showing

* chore: 🤖 fix yarn.lock
2024-09-04 13:04:14 -07:00
hashicorp-tsccr[bot]andhashicorp-tsccr[bot] 0cfa814cdd Result of tsccr-helper -log-level=info gha update -latest . (#2469)
Co-authored-by: hashicorp-tsccr[bot] <hashicorp-tsccr[bot]@users.noreply.github.com>
2024-09-04 11:41:24 -07:00
Lisbet Alvarez 694c43296a chore: 🤖 remove request unused dependency resolution (#2467)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14214
2024-08-29 12:17:27 -07:00
Lisbet Alvarez c98bfb5622 chore: 🤖 upgrade jsonpointer to v5 (#2465)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14212
2024-08-28 09:33:47 -07:00
Michael Li 7cebece600 refact(e2e): Use fixtures for environment variables (#2452) 2024-08-28 11:54:58 -04:00
WadiandCameron Perera a9cfc43404 Icu 14663 update manage dropdown for credential stores form (#2454)
* refactor: 💡 Update Manage dropdown in credential-store

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

* refactor: 💡 Adjusted tests

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

* refactor: 💡 Refactored tests and naming conventions

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

* refactor: 💡 Removed @model from delete

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

* test: 💍 add clicks to correct tests assertions

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

---------

Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
2024-08-26 14:58:14 -05:00
WadiandCameron Perera c38ba294cc refactor: 💡 Update Manage Dropdown in auth-methods (#2460)
* refactor: 💡 Update Manage Dropdown in auth-methods

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

* refactor: 💡 Refactored tests

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

---------

Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
2024-08-26 14:30:57 -05:00
Cameron Perera ce2649272f chore: 🤖 upgrade ember-qunit to v8.1.0 (#2462)
and ember-cli to 4.12.2 in addons/api
2024-08-26 14:13:45 -05:00
WadiandCameron Perera 3d60d047e3 refactor: 💡 Manage dropdown HDS refactoring for managed-groups (#2455)
* refactor: 💡 Manage dropdown HDS refactoring for managed-groups

Updated the manage dropdown to reflect HDS design

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

* refactor: 💡 Changed up naming convention for Manage dropdown

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

---------

Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
2024-08-26 10:48:36 -05:00
WadiandCameron Perera a3082e97e6 refactor: 💡 Update manage dropdown for host-catalogs (#2456)
* refactor: 💡 Update manage dropdown for host-catalogs

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

* refactor: 💡 Refactored tests

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

* refactor: 💡 Refactored test

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

---------

Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
2024-08-26 09:44:51 -05:00
dependabot[bot]andZhihe Li 333eebd731 chore(deps): bump micromatch in /ui/desktop/electron-app (#2461)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/4.0.8/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  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-08-23 16:36:41 -04:00
Lisbet Alvarez 320af9f6f7 chore: 🤖 upgrade to ws v8.17.1 to remove vulnerability (#2451)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14204
2024-08-23 13:25:10 -07:00
Cameron Perera af9b832d38 chore: 🤖 update ember-cli-mirage (#2458)
* chore: 🤖 update ember-cli-mirage

* chore: 🤖 move ember-cli-mirage to dependencies

* chore: 🤖 remove duplicate dependency
2024-08-23 14:40:42 -05:00
Wadi bea791b2e6 refactor: 💡 Update Manage dropdown in Credentials form (#2453)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14665
2024-08-23 12:03:26 -04:00
Wadi e31ac72531 ICU-14870 Update Manage Dropdown for Projects form (#2457)
* refactor: 💡 HDS Manage Dropdown for Projects

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

* refactor: 💡 Fixing up tests

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

* refactor: 💡 Updated naming convention and consolidated

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14870
2024-08-23 11:53:26 -04:00
Michael Li 075d8c6d8f refact(e2e): Create HTTP methods and more page objects (#2448)
* refact(e2e): Create LoginPage

* refact(e2e): Create http methods

* refact(e2e): Create more methods
2024-08-23 10:17:10 -04:00
Lauren 56f42c3f5a docs: ✏️ replace broken links, add hyperlinks, language updates (#2445)
This PR updates the repo READMEs by replacing a broken link and pointing to correct tutorials page, adding hyperlinks to improve readability, and small language updates
2024-08-20 16:14:19 -04:00
Carlos Alcaide Corvo af7196215b chore: 🤖 Migrate xterm to @xterm (#2444) 2024-08-20 09:40:08 -07:00
dependabot[bot]andDharini Jeeva 80d2898e56 chore(deps): bump elliptic from 6.5.4 to 6.5.7 (#2443)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.4 to 6.5.7.
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.4...v6.5.7)

---
updated-dependencies:
- dependency-name: elliptic
  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: Dharini Jeeva <dharini@hashicorp.com>
2024-08-20 09:09:14 -05:00
Carlos Alcaide Corvo f607e77274 Automate copyright headers addition (#2440)
* Refactor checking headers to automatically add them
2024-08-19 14:22:27 -07:00
Michael Li 007f5d8b46 refact(e2e): Split up boundary-cli helper methods (#2430)
* refact(e2e): Split up boundary-cli helper methods

* CR: Use ESM import

* CR: Move authenticated state to a non-shared file

* CR: Use mjs

* CR: Modify playwright config to .mjs
2024-08-19 16:00:16 -04:00
Lisbet Alvarez a9d110a1b6 refactor: 💡 remove route-action usage for session recordings (#2439)
* refactor: 💡 remove route-action usage for session recordings

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12620
2024-08-16 10:04:46 -07:00
Lisbet AlvarezandCameron Perera 79b06ee565 ICU-13883 add remove org modal (#2435)
* feat: 🎸 add remove org modal

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

---------

Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
2024-08-15 16:04:46 -07:00
Lisbet Alvarez 0af81d9bc1 refactor: 💡 remove route action usage for scopes (#2385)
* refactor: 💡 remove route action usage for scopes

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13755
2024-08-15 10:46:00 -07:00
Cameron Perera 9e668e8467 Icu 14834 remove duplicate selectors and use common selectors instead (#2436)
* refactor: 💡 update SAVE_BTN usage

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

* refactor: 💡 update CANCEL_BTN usage

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

* refactor: 💡 update EDIT_BTN usage

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

* refactor: 💡 update FIELD_NAME & FIELD_NAME_VALUE usage

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14834
2024-08-15 09:24:21 -05:00
Wadi 69da1fa04c refactor: 💡 Updated Manage Dropdown in auth-methods accounts (#2432)
* refactor: 💡 Updated Manage Dropdown in auth-methods accounts

Updated Manage Dropdown in auth-methods account form to HDS

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

* refactor: 💡 Changed asssert statement in delete-test.js

Changed statement to reflect proper syntax and testing capability

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14599
2024-08-12 13:52:56 -04:00
Wadi 392792eb92 refactor: 💡 Updated Manage Dropdown in credential-library (#2434)
Updated Manage Dropdown present in credential-library to reflect new HDS
design

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14601
2024-08-12 13:41:08 -04:00
Wadi 9c018f131f refactor: 💡 Updated Manage Dropdown in host-sets to HDS design (#2433)
* refactor: 💡 Updated Manage Dropdown in host-sets to HDS design

Updated the manage dropdown in host-sets to reflect HDS design

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

* refactor: 💡 Specified selector in host-test.js

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14600
2024-08-12 13:25:43 -04:00
Cameron Perera 0542218537 Icu 14033 storage buckets read test (#2414)
* test: 💍 update storage-bucket read test selectors

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

* test: 💍 add common selectors in tests/helpers

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

* refactor: 💡 update selector name and value

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

* refactor: 💡 update selector name

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14033
2024-08-12 10:41:27 -05:00
Lisbet Alvarez 7ac904a31f ICU-14496 add discard unsaved changes modal to manage scopes (#2421)
* refactor: 💡 add discard unsaved changes modal

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14496
2024-08-08 10:46:39 -07:00
Lisbet Alvarez a7d9a7e348 fix: 🐛 targets not displaying even after they are cached (#2429)
* fix: 🐛 targets not displaying even after they are cached

* fix: 🐛 modify check in sessions route
2024-08-07 10:38:38 -07:00
Michael Li 0d0dadf398 chore(e2e): Fix eslint for Admin UI e2e tests (#2427)
* chore(e2e): Fix eslint for e2e tests

* chore(e2e): Fix license headers
2024-08-06 14:26:19 -04:00
Michael Li 45c16252a7 refact(e2e): Update Admin UI tests to use Page Objects (#2426)
* refact(e2e): Use page object methods

* refact(e2e): Create base page class
2024-08-06 13:15:00 -04:00
Wadi 9b3cbaa6b3 refactor: 💡 Updated Manage Dropdown to HDS in Targets Menu (#2418)
* refactor: 💡 Updated Manage Dropdown to HDS in Targets Menu

Updated the manage dropdown present in the targets scope to reflect HDS
design

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

* refactor: 💡 Updated to reflect changes

Added changes to reflect variable design

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

* refactor: 💡 Removed @deleted attribute

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14597
2024-08-05 13:04:02 -04:00
Wadi 89cfb14d6e refactor: 💡 Updated Manage Dropdown for hosts form (#2423)
* refactor: 💡 Updated Manage Dropdown for hosts form

Continuing refactoring upgrades for HDS managed dropdowns

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

* refactor: 💡 Removed disabled attribute from dropdown

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14602
2024-08-05 12:53:55 -04:00
Zhihe Li a68d7d103a feat: 🎸 Setup initial desktop E2E tests (#2416)
* feat: 🎸 Setup initial desktop E2E tests

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14061
2024-08-02 13:28:50 -04:00
hashicorp-tsccr[bot]andhashicorp-tsccr[bot] 4dc5e13f24 Result of tsccr-helper -log-level=info gha update -latest . (#2422)
Co-authored-by: hashicorp-tsccr[bot] <hashicorp-tsccr[bot]@users.noreply.github.com>
2024-08-01 10:13:24 -07:00
457 changed files with 16065 additions and 12903 deletions

No files matched your search

+8 -8
View File
@@ -19,7 +19,7 @@ jobs:
key: node-modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
node-modules-
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with:
node-version: 20
- run: yarn install
@@ -37,17 +37,17 @@ jobs:
restore-keys: |
node-modules-
- name: Build
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with:
node-version: 20
- run: yarn run build:ui:admin:oss
- name: Upload artifact admin-ui
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: admin-ui
path: ./ui/admin/dist/
- name: Upload artifact OSS admin UI
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: admin-ui-oss
path: ./ui/admin/dist/
@@ -65,12 +65,12 @@ jobs:
restore-keys: |
node-modules-
- name: Build
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with:
node-version: 20
- run: yarn run build:ui:admin:enterprise
- name: Upload artifact enterprise admin UI
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: admin-ui-ent
path: ./ui/admin/dist/
@@ -88,12 +88,12 @@ jobs:
restore-keys: |
node-modules-
- name: Build
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with:
node-version: 20
- run: yarn run build:ui:admin:hcp
- name: Upload artifact HCP admin UI
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: admin-ui-hcp
path: ./ui/admin/dist/
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8f1a6fed33af5212fab8a999d004627ae8901d1b # codeql-bundle-v2.17.5
uses: github/codeql-action/init@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0
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@8f1a6fed33af5212fab8a999d004627ae8901d1b # codeql-bundle-v2.17.5
uses: github/codeql-action/analyze@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0
+8 -3
View File
@@ -1,10 +1,15 @@
name: Check Copyright Headers
name: Add Copyright Headers
on: pull_request
jobs:
copywrite:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3
- name: Check Header Compliance
run: copywrite headers --plan
- name: Add missing Copyright Headers
run: copywrite headers
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
with:
commit_message: Add missing copyright headers
+4 -4
View File
@@ -14,7 +14,7 @@ jobs:
key: node-modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
node-modules-
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with:
node-version: 20
- run: yarn install
@@ -32,7 +32,7 @@ jobs:
key: node-modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
node-modules-
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with:
node-version: 20
- run: yarn run compliance:licenses
@@ -51,7 +51,7 @@ jobs:
key: node-modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
node-modules-
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with:
node-version: 20
- run: yarn lint
@@ -68,7 +68,7 @@ jobs:
with:
path: '**/node_modules'
key: node-modules-${{ hashFiles('**/yarn.lock') }}
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with:
node-version: 20
- run: yarn test
+10 -9
View File
@@ -98,7 +98,7 @@ List of available project commands. `yarn run <command-name>`
| Command | Description |
|---------------------|---|
| build | Builds Admin UI. |
| build | Builds Admin UI for production. |
| test | Runs tests in CI mode. |
| lint | Runs ember-template-lint on all of the hbs, js, and sass files. |
| compliance:audit | Checks for issues using `yarn audit` with moderate and above criticality.|
@@ -119,9 +119,9 @@ of README.
### Building Admin UI for Production
See ui/admin/README.md Building for Production for more information on how to
build the Admin UI. Be sure to set build-related environment variables as
necessary for your target environment, as described in the Admin UI readme.
See [Admin UI README](ui/admin/README.md#building-for-production) for more information on how to
build the Admin UI for production. Be sure to set build-related environment variables as
necessary for your target environment, as described in the [Environment Variables](ui/admin/README.md#environment-variables) section of README.
To quickly produce a production build of Admin UI, run from this folder:
@@ -144,9 +144,9 @@ Assets are saved to `ui/admin/dist/`.
### Building Desktop UI for Production
See ui/desktop/README.md Building for Production for more information on how to
See [Desktop UI README](ui/desktop/README.md#building-for-production) for more information on how to
build the Desktop UI. Be sure to set build-related environment variables as
necessary for your target environment, as described in the Desktop UI readme.
necessary for your target environment, as described in the [Environment Variables](ui/desktop/README.md#environment-variables-prod) section of README.
To quickly produce a production build of Desktop UI, run from this folder:
@@ -174,8 +174,9 @@ EXE archived desktop UI is available at asset location under `zip` folder.
This describes how to connect local Boundary UI to your local instance of Boundary.
This assumes you already have up and running a Boundary dev instance in your local environment with a listener `127.0.0.1:9200` (default behaviour). [Learn how to start a local Boundary dev instance](https://learn.hashicorp.com/tutorials/boundary/getting-started-dev?in=boundary/getting-started#start-a-dev-environment).
#### For admin
This assumes you already have up and running a Boundary dev instance in your local environment with a listener `127.0.0.1:9200` (default behaviour). [Learn how to start a local Boundary dev instance](https://developer.hashicorp.com/boundary/tutorials/get-started-community/community-get-started-dev)
#### For Admin
This assumes you are within `boundary-ui/ui/admin`.
@@ -185,7 +186,7 @@ Following terminal instructions, open in your browser: `http://localhost:4200/`.
Once you open the UI you will see the login screen, authenticate using generated admin login name and password.
#### For desktop
#### For Desktop
This assumes you are within `boundary-ui/ui/desktop`.
@@ -7,6 +7,7 @@ import sinon from 'sinon';
import { typeOf } from '@ember/utils';
import { resourceNames } from 'api/handlers/cache-daemon-handler';
import { singularize } from 'ember-inflector';
import { underscore } from '@ember/string';
/**
* This test helper can be used to help setup your sinon stubs in your tests.
@@ -69,16 +70,18 @@ export default function setupStubs(hooks) {
.withArgs('searchCacheDaemon')
.onCall(i)
.returns({
[resourceName]: models.map((model) => {
// Use internal serializer to serialize the model correctly
// according to our mirage serializers
const modelData =
this.server.serializerOrRegistry.serialize(model);
[underscore(resourceNames[singularize(resourceName)])]: models.map(
(model) => {
// Use internal serializer to serialize the model correctly
// according to our mirage serializers
const modelData =
this.server.serializerOrRegistry.serialize(model);
// Serialize the data properly to standard JSON as that is what
// we're expecting from the cache daemon response
return JSON.parse(JSON.stringify(modelData));
}),
// Serialize the data properly to standard JSON as that is what
// we're expecting from the cache daemon response
return JSON.parse(JSON.stringify(modelData));
},
),
});
});
};
@@ -65,12 +65,19 @@ export default class GeneratedHostCatalogModel extends BaseModel {
// AWS specific
@attr('string', {
description:
'An expression used to filter the workers that have network access to a service that is hosting the external object store.',
})
worker_filter;
@attr('string', {
isNestedAttribute: true,
description: '',
})
region;
// AWS static credentials
@attr('string', {
isNestedSecret: true,
description: '',
@@ -83,6 +90,31 @@ export default class GeneratedHostCatalogModel extends BaseModel {
})
secret_access_key;
// AWS dynamic credentials
@attr('string', {
isNestedAttribute: true,
description: 'The role ARN to use.',
})
role_arn;
@attr('string', {
isNestedAttribute: true,
description: 'The role external ID to use.',
})
role_external_id;
@attr('string', {
isNestedAttribute: true,
description: 'The role session to use.',
})
role_session_name;
@attr('object-as-array', {
isNestedAttribute: true,
description: 'The role tags to use.',
})
role_tags;
// Azure specific
@attr('string', {
@@ -8,6 +8,7 @@ import { inject as service } from '@ember/service';
import { getOwner, setOwner } from '@ember/application';
import { generateMQLExpression } from '../utils/mql-query';
import { paginateResults } from '../utils/paginate-results';
import { underscore } from '@ember/string';
/**
* Not all types are yet supported by the cache daemon so we'll
@@ -94,12 +95,14 @@ export default class CacheDaemonHandler {
const sessionData = this.session.data?.authenticated;
const auth_token_id = sessionData?.id;
const token = sessionData?.token;
const resourceName = resourceNames[type];
remainingQuery = {
...remainingQuery,
query: searchQuery,
auth_token_id,
token,
resource: resourceNames[type],
resource: resourceName,
};
let cacheDaemonResults = {};
@@ -130,7 +133,7 @@ export default class CacheDaemonHandler {
}
} else {
__electronLog?.error(
'Failed to search cache daemon; falling back to search controller',
`Failed to search cache daemon for ${resourceName}. Error:`,
e.message,
);
@@ -138,10 +141,7 @@ export default class CacheDaemonHandler {
}
}
// Currently returns with a singular top level field with resource name
// e.g. { targets: [...] } or { sessions: [..] }
// So this just unwraps to the array, or undefined
const [results] = Object.values(cacheDaemonResults);
const results = cacheDaemonResults[underscore(resourceName)];
const payload = { items: paginateResults(results, page, pageSize) };
const schema = store.modelFor(type);
@@ -167,7 +167,16 @@ export default class CacheDaemonHandler {
// 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: results?.length ?? 0 };
// Also include whether the results are incomplete or is still refreshing
// as part of the response
records.meta = {
totalItems: results?.length ?? 0,
isLoadIncomplete: cacheDaemonResults.incomplete ?? false,
// Sometimes the refresh status returns an error status if it takes too long but it's still refreshing
isCacheRefreshing:
cacheDaemonResults.refresh_status !== 'not-refreshing',
};
return records;
}
default:
@@ -1,27 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
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,
};
+32
View File
@@ -19,9 +19,18 @@ export const TYPES_HOST_CATALOG_PLUGIN = [
TYPE_HOST_CATALOG_PLUGIN_AZURE,
];
export const TYPE_CREDENTIAL_STATIC = 'static-credential';
export const TYPE_CREDENTIAL_DYNAMIC = 'dynamic-credential';
export const TYPES_CREDENTIALS = Object.freeze([
TYPE_CREDENTIAL_STATIC,
TYPE_CREDENTIAL_DYNAMIC,
]);
export default class HostCatalogModel extends GeneratedHostCatalogModel {
// =attributes
#credentialType;
/**
* True if the host catalog is static.
* @type {boolean}
@@ -38,6 +47,29 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
return this.type === 'plugin';
}
/**
* Gets a value from credentialType
* @type {string}
*/
get credentialType() {
if (!this.#credentialType) {
if (this.role_arn) {
this.#credentialType = TYPE_CREDENTIAL_DYNAMIC;
} else {
this.#credentialType = TYPE_CREDENTIAL_STATIC;
}
}
return this.#credentialType;
}
/**
* Sets a value to credentialType
* @type {string}
*/
set credentialType(type) {
this.#credentialType = type;
}
/**
* True if the host catalog is an unknown type.
* @type {boolean}
+63 -23
View File
@@ -4,29 +4,59 @@
*/
import ApplicationSerializer from './application';
import {
TYPE_HOST_CATALOG_PLUGIN_AWS,
TYPE_HOST_CATALOG_PLUGIN_AZURE,
TYPE_CREDENTIAL_DYNAMIC,
} from '../models/host-catalog';
const fieldsByType = {
aws: [
'disable_credential_rotation',
'region',
// These fields are Azure specific
const azureFields = [
'disable_credential_rotation',
'tenant_id',
'client_id',
'subscription_id',
'secret_id',
'secret_value',
];
// These fields are AWS specific, AWS provider now has two credentialTypes: static and dynamic [not to be confused with static and dynamic host-catalogs]
const AWSfieldsWithCredentialType = {
'static-credential': [
'access_key_id',
'secret_access_key',
],
azure: [
'region',
'disable_credential_rotation',
'tenant_id',
'client_id',
'subscription_id',
'secret_id',
'secret_value',
'worker_filter',
],
'dynamic-credential': [
'role_arn',
'role_external_id',
'role_session_name',
'role_tags',
'region',
'disable_credential_rotation',
'worker_filter',
],
};
export default class HostCatalogSerializer extends ApplicationSerializer {
// =methods
serialize(snapshot) {
const { compositeType, credentialType } = snapshot.record;
const serialized = super.serialize(...arguments);
switch (snapshot.record.compositeType) {
// This is a hack to remove the worker_filter field from the serialized data if the compositeType is not AWS
// Ideally we should refactor the model to include the compositeType using `for` attribute
if (compositeType !== TYPE_HOST_CATALOG_PLUGIN_AWS) {
delete serialized.worker_filter;
}
// By default, disable credential rotation for dynamic credentials
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
serialized.attributes.disable_credential_rotation = true;
}
switch (compositeType) {
case 'static':
return this.serializeStatic(...arguments);
default:
@@ -36,21 +66,30 @@ export default class HostCatalogSerializer extends ApplicationSerializer {
serializeAttribute(snapshot, json, key, attribute) {
const value = super.serializeAttribute(...arguments);
const { isPlugin, compositeType } = snapshot.record;
const { compositeType, credentialType, isPlugin } = snapshot.record;
const { options } = attribute;
// Nested conditionals for readability
// This deletes any fields that don't belong to the record type
const fields =
compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE
? azureFields
: AWSfieldsWithCredentialType[credentialType];
// Delete any nested attribute fields that don't belong to the record type
if (isPlugin && options.isNestedAttribute && json.attributes) {
// The key must be included in the fieldsByType list above
if (!fieldsByType[compositeType].includes(key))
if (!fields.includes(key)) {
delete json.attributes[key];
}
// This deletes any fields that don't belong to the record type
if (isPlugin && options.isNestedSecret && json.secrets) {
if (!fieldsByType[compositeType].includes(key)) delete json.secrets[key];
}
}
// new one for isNestedSecret
// Delete any secret fields that don't belong to the record type
if (isPlugin && options.isNestedSecret && json.secrets) {
if (!fields.includes(key)) {
delete json.secrets[key];
if (json['secrets'] && Object.keys(json.secrets).length === 0) {
delete json.secrets;
}
}
}
return value;
}
@@ -59,6 +98,7 @@ export default class HostCatalogSerializer extends ApplicationSerializer {
// Delete unnecessary fields for static host-catalog
delete serialized.attributes;
delete serialized.secrets;
delete serialized.worker_filter;
return serialized;
}
}
+5 -1
View File
@@ -29,6 +29,10 @@ export const modelIndexes = {
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id, attributes.plugin.name',
'auth-method':
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.is_primary, attributes.scope.scope_id',
'session-recording':
'&id, attributes.created_time, attributes.type, attributes.state, attributes.start_time, attributes.end_time, attributes.duration, attributes.scope.scope_id, attributes.create_time_values.user.id, attributes.create_time_values.user.name, attributes.create_time_values.target.id, attributes.create_time_values.target.name, attributes.create_time_values.target.scope.id, attributes.create_time_values.target.scope.name',
alias:
'&id, attributes.created_time, attributes.type, attributes.value, attributes.name, attributes.description, attributes.destination_id, attributes.scope.scope_id',
};
export const formatDbName = (userId, clusterUrl) =>
@@ -109,7 +113,7 @@ export default class IndexedDbService extends Service {
}
this.#db = new Dexie(dbName);
this.#db.version(1).stores(modelIndexes);
this.#db.version(2).stores(modelIndexes);
}
/**
+5 -13
View File
@@ -25,7 +25,10 @@ const isTesting = environmentConfig.environment === 'test';
export default function (mirageConfig) {
let finalConfig = {
...mirageConfig,
models: { ...discoverEmberDataModels(), ...mirageConfig.models },
models: {
...discoverEmberDataModels(mirageConfig.store),
...mirageConfig.models,
},
serializers: applyEmberDataSerializers(mirageConfig.serializers),
routes,
};
@@ -775,18 +778,7 @@ function routes() {
});
// session recordings
this.get(
'/session-recordings',
(
{ sessionRecordings },
{ queryParams: { scope_id: scopeId, recursive } },
) => {
if (recursive && scopeId === 'global') {
return sessionRecordings.all();
}
return sessionRecordings.where({ scopeId });
},
);
this.get('/session-recordings');
this.get(
'/session-recordings/:idMethod',
async ({ sessionRecordings }, { params: { idMethod } }) => {
@@ -38,6 +38,12 @@ export default factory.extend({
};
},
worker_filter: function () {
if (this.type === 'plugin' && this.plugin?.name === 'aws') {
return `"${faker.word.noun()}" in "${faker.system.directoryPath()}"`;
}
},
plugin: function (i) {
if (this.type === 'plugin') {
return {
+21
View File
@@ -190,6 +190,27 @@ export default function initializeMockIPC(server, config) {
isClientAgentRunning() {
return false;
}
cacheDaemonStatus() {
return { version: 'Boundary v1.0.0' };
}
getCliVersion() {
return { versionNumber: '1.0.0' };
}
getDesktopVersion() {
return { desktopVersion: '1.0.0' };
}
getLogLevel() {
return 'info';
}
getLogPath() {
return '~/.config/Boundary/logs/desktop-client.log';
}
setLogLevel() {}
clientAgentStatus() {
return { version: '0.0.1-dev', status: 'running' };
}
pauseClientAgent() {}
resumeClientAgent() {}
}
/**
+10 -10
View File
@@ -26,35 +26,36 @@
"dexie": "^3.2.4",
"ember-auto-import": "^2.6.3",
"ember-can": "^4.2.0",
"ember-cli-babel": "^7.26.10",
"ember-cli-babel": "^8.2.0",
"ember-cli-htmlbars": "^6.2.0",
"ember-cli-mirage": "^2.4.0",
"ember-cli-mirage": "^3.0.3",
"miragejs": "^0.1.48",
"ember-data": "~4.12.0",
"ember-fetch": "^8.1.1",
"tracked-built-ins": "^3.1.1"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/core": "^7.25.2",
"@babel/eslint-parser": "^7.21.3",
"@babel/plugin-proposal-decorators": "^7.21.0",
"@ember/optional-features": "^2.0.0",
"@ember/string": "^3.0.1",
"@ember/test-helpers": "^2.9.3",
"@ember/test-helpers": "^3.3.1",
"@embroider/test-setup": "^2.1.1",
"@faker-js/faker": "^8.0.2",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
"broccoli-asset-rev": "^3.0.0",
"concurrently": "^8.0.1",
"doctoc": "^2.2.0",
"ember-cli": "^4.12.1",
"ember-cli-dependency-checker": "^3.3.1",
"ember-cli": "^4.12.2",
"ember-cli-dependency-checker": "^3.3.2",
"ember-cli-inject-live-reload": "^2.1.0",
"ember-cli-sri": "^2.1.1",
"ember-cli-terser": "^4.0.2",
"ember-exam": "^8.0.0",
"ember-load-initializers": "^2.1.2",
"ember-page-title": "^7.0.0",
"ember-qunit": "^6.2.0",
"ember-qunit": "^8.1.0",
"ember-resolver": "^10.0.0",
"ember-source": "~4.12.0",
"ember-source-channel-url": "^3.0.0",
@@ -64,13 +65,12 @@
"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",
"qunit": "^2.19.4",
"qunit-dom": "^2.0.0",
"webpack": "^5.78.0"
"webpack": "^5.94.0"
},
"peerDependencies": {
"ember-source": ">4.0.0"
+1 -1
View File
@@ -28,7 +28,7 @@ function setupApplicationTest(hooks, options) {
// This is also a good place to call test setup functions coming
// from other addons:
//
// setupIntl(hooks); // ember-intl
// setupIntl(hooks, 'en-us'); // ember-intl
// setupMirage(hooks); // ember-cli-mirage
}
@@ -1,42 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
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);
});
});
@@ -5,6 +5,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_HOST_CATALOG_PLUGIN_AWS,
TYPE_CREDENTIAL_STATIC,
TYPE_CREDENTIAL_DYNAMIC,
} from 'api/models/host-catalog';
module('Unit | Serializer | host catalog', function (hooks) {
setupTest(hooks);
@@ -31,13 +36,103 @@ module('Unit | Serializer | host catalog', function (hooks) {
assert.deepEqual(record.serialize(), expectedResult);
});
test('it serializes a new aws plugin as expected, ignoring azure fields', async function (assert) {
test('it serializes a new aws static plugin as expected', async function (assert) {
const store = this.owner.lookup('service:store');
const record = store.createRecord('host-catalog', {
compositeType: 'aws',
compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS,
credentialType: TYPE_CREDENTIAL_STATIC,
name: 'AWS',
description: 'this is a Aws plugin host-catalog',
disable_credential_rotation: true,
worker_filter: 'workerfilter',
// these are AWS fields and should be included
region: 'west',
access_key_id: 'foobars',
secret_access_key: 'testing',
// these are Azure fields and should be excluded
tenant_id: 'a1b2c3',
client_id: 'a1b2c3',
subscription_id: 'a1b2c3',
secret_id: 'a1b2c3',
secret_value: 'a1b2c3',
role_arn: 'test',
role_external_id: 'test',
role_session_name: 'test',
role_tags: [
{ key: 'Project', value: 'Automation' },
{ key: 'foo', value: 'bar' },
],
});
const expectedResult = {
name: 'AWS',
description: 'this is a Aws plugin host-catalog',
type: 'plugin',
worker_filter: 'workerfilter',
attributes: {
disable_credential_rotation: true,
region: 'west',
},
secrets: {
access_key_id: 'foobars',
secret_access_key: 'testing',
},
};
assert.deepEqual(record.serialize(), expectedResult);
});
test('it serializes a new aws dynamic plugin as expected', async function (assert) {
const store = this.owner.lookup('service:store');
const record = store.createRecord('host-catalog', {
compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS,
name: 'AWS',
credentialType: TYPE_CREDENTIAL_DYNAMIC,
description: 'this is a Aws plugin host-catalog',
disable_credential_rotation: true,
worker_filter: 'workerfilter',
// these are AWS fields and should be included
region: 'west',
access_key_id: 'foobars',
secret_access_key: 'testing',
// these are Azure fields and should be excluded
tenant_id: 'a1b2c3',
client_id: 'a1b2c3',
subscription_id: 'a1b2c3',
secret_id: 'a1b2c3',
secret_value: 'a1b2c3',
role_arn: 'test',
role_external_id: 'test',
role_session_name: 'test',
role_tags: [
{ key: 'Project', value: 'Automation' },
{ key: 'foo', value: 'bar' },
],
});
const expectedResult = {
name: 'AWS',
description: 'this is a Aws plugin host-catalog',
type: 'plugin',
worker_filter: 'workerfilter',
attributes: {
disable_credential_rotation: true,
region: 'west',
role_external_id: 'test',
role_session_name: 'test',
role_arn: 'test',
role_tags: { Project: 'Automation', foo: 'bar' },
},
};
assert.deepEqual(record.serialize(), expectedResult);
});
test('it serializes a new aws static plugin as expected, ignoring azure fields', async function (assert) {
const store = this.owner.lookup('service:store');
const record = store.createRecord('host-catalog', {
compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS,
name: 'AWS',
credentialType: TYPE_CREDENTIAL_STATIC,
description: 'this is a Aws plugin host-catalog',
disable_credential_rotation: true,
worker_filter: 'workerfilter',
// these are AWS fields and should be included
region: 'spain',
access_key_id: 'foobars',
@@ -53,6 +148,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
name: 'AWS',
description: 'this is a Aws plugin host-catalog',
type: 'plugin',
worker_filter: 'workerfilter',
attributes: {
disable_credential_rotation: true,
region: 'spain',
@@ -75,6 +171,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
region: 'spain',
access_key_id: 'foobars',
secret_access_key: 'testing',
worker_filter: 'workerfilter',
// these are Azure fields and should be excluded
tenant_id: 'a1b2c3',
client_id: 'a1b2c3',
@@ -106,6 +203,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
data: {
id: '1',
type: 'host-catalog',
credentialType: 'static',
attributes: {
type: 'plugin',
name: 'aws',
@@ -113,6 +211,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
plugin: {
name: 'aws',
},
worker_filter: 'workerfilter',
region: 'andorra',
},
},
@@ -122,6 +221,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
type: 'plugin',
name: 'aws',
description: 'test description',
worker_filter: 'workerfilter',
attributes: {
disable_credential_rotation: false,
region: 'andorra',
@@ -215,6 +315,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
name: 'aws',
description: 'aws host catalog',
},
worker_filter: 'workerfilter',
attributes: {
disable_credential_rotation: false,
region: 'Illinois',
@@ -238,6 +339,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
type: 'plugin',
description: 'Test description',
authorized_actions: ['no-op'],
worker_filter: 'workerfilter',
plugin: {
id: 'plugin-id-5',
name: 'aws',
@@ -11,6 +11,7 @@ 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';
import { faker } from '@faker-js/faker';
const seedIndexDb = async (resource, store, indexedDb, server) => {
const resourceData =
@@ -529,4 +530,80 @@ module('Unit | Utility | indexed-db-query', function (hooks) {
assert.strictEqual(result.length, 0);
});
test('it filters on date type fields and returns results', async function (assert) {
const pastDate = new Date('2024-09-19T10:00:00.000Z');
const createdTime = new Date('2024-09-23T10:00:00.000Z');
this.server.create('session-recording', {
created_time: createdTime,
});
await seedIndexDb('session-recording', store, indexedDb, this.server);
const query = { filters: { created_time: [{ gte: pastDate }] } };
const result = await queryIndexedDb(
indexedDb.db,
'session-recording',
query,
);
assert.strictEqual(result.length, 1);
});
test('it filters on date type fields and returns results - from and to', async function (assert) {
const from = new Date('2024-09-19T10:00:00.000Z');
const to = new Date('2024-09-25T10:00:00.000Z');
const createdTime = new Date('2024-09-23T10:00:00.000Z');
const createdTime2 = new Date('2024-09-28T10:00:00.000Z');
this.server.create('session-recording', {
created_time: createdTime,
});
this.server.create('session-recording', {
created_time: createdTime2,
});
await seedIndexDb('session-recording', store, indexedDb, this.server);
const query = {
filters: {
created_time: {
logicalOperator: 'and',
values: [{ gte: from }, { lte: to }],
},
},
};
const result = await queryIndexedDb(
indexedDb.db,
'session-recording',
query,
);
assert.strictEqual(result.length, 1);
assert.strictEqual(
result[0].attributes.created_time.toISOString(),
createdTime.toISOString(),
);
});
test('it filters on date type fields and returns no results', async function (assert) {
const createdTime = faker.date.between({
from: '2024-09-19T00:00:00.000Z',
to: '2024-09-20T00:00:00.000Z',
});
const pastDate = faker.date.between({
from: '2024-09-23T00:00:00.000Z',
to: '2024-09-24T00:00:00.000Z',
});
this.server.create('session-recording', {
created_time: createdTime,
});
await seedIndexDb('session-recording', store, indexedDb, this.server);
const query = { filters: { created_time: [{ gte: pastDate }] } };
const result = await queryIndexedDb(
indexedDb.db,
'session-recording',
query,
);
assert.strictEqual(result.length, 0);
});
});
+18 -12
View File
@@ -5,7 +5,7 @@
import SimpleAuthBaseAuthenticator from 'ember-simple-auth/authenticators/base';
import { resolve, reject } from 'rsvp';
import fetch from 'fetch';
import { waitForPromise } from '@ember/test-waiters';
/**
* Encapsulates common authenticator functionality.
@@ -58,10 +58,14 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
*/
async validateToken(token, tokenID) {
const tokenValidationURL = this.buildTokenValidationEndpointURL(tokenID);
const response = await fetch(tokenValidationURL, {
method: 'get',
headers: { Authorization: `Bearer ${token}` },
});
// Note: waitForPromise is needed to provide the necessary integration with @ember/test-helpers
// visit https://www.npmjs.com/package/@ember/test-waiters for more info.
const response = await waitForPromise(
fetch(tokenValidationURL, {
method: 'get',
headers: { Authorization: `Bearer ${token}` },
}),
);
// 401 and 404 responses mean the token is invalid, whereas other types of
// error responses do not tell us about the validity of the token.
if (response.status === 401 || response.status === 404) return reject();
@@ -103,7 +107,7 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
}
/**
* Posts to the `deauthEntpoint` on a best-effort basis and then returns.
* Posts to the `deauthEndpoint` on a best-effort basis and then returns.
* Deauthentication with the server is not guaranteed and request failures
* are ignored.
*
@@ -113,12 +117,14 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
async invalidate(options) {
const { token } = options;
const deauthEndpointURL = this.buildDeauthEndpointURL(options);
await fetch(deauthEndpointURL, {
method: 'delete',
headers: { Authorization: `Bearer ${token}` },
}).catch(() => {
/* no op */
});
await waitForPromise(
fetch(deauthEndpointURL, {
method: 'delete',
headers: { Authorization: `Bearer ${token}` },
}).catch(() => {
/* no op */
}),
);
return super.invalidate(...arguments);
}
}
+5 -3
View File
@@ -6,7 +6,7 @@
import BaseAuthenticator from './base';
import { inject as service } from '@ember/service';
import { reject } from 'rsvp';
import fetch from 'fetch';
import { waitForPromise } from '@ember/test-waiters';
/**
* The OIDC base authenticator encapsulates the multistep OIDC flow.
@@ -47,7 +47,9 @@ export default class OIDCAuthenticator extends BaseAuthenticator {
async startAuthentication(options) {
const url = this.buildAuthEndpointURL(options);
const body = JSON.stringify({ command: 'start' });
const response = await fetch(url, { method: 'post', body });
// Note: waitForPromise is needed to provide the necessary integration with @ember/test-helpers
// visit https://www.npmjs.com/package/@ember/test-waiters for more info.
const response = await waitForPromise(fetch(url, { method: 'post', body }));
const json = await response.json();
if (response.status < 400) {
// Store meta about the pending OIDC flow
@@ -94,7 +96,7 @@ export default class OIDCAuthenticator extends BaseAuthenticator {
},
});
// Fetch the endpoint and get the response JSON
const response = await fetch(url, { method: 'post', body });
const response = await waitForPromise(fetch(url, { method: 'post', body }));
if (response.status === 202) {
// The token isn't ready yet, keep trying.
return false;
+6 -2
View File
@@ -5,7 +5,7 @@
import BaseAuthenticator from './base';
import { resolve, reject } from 'rsvp';
import fetch from 'fetch';
import { waitForPromise } from '@ember/test-waiters';
/**
*
@@ -66,7 +66,11 @@ export default class PasswordAuthenticator extends BaseAuthenticator {
attributes: { login_name, password },
});
const authEndpointURL = this.buildAuthEndpointURL(options);
const response = await fetch(authEndpointURL, { method: 'post', body });
// Note: waitForPromise is needed to provide the necessary integration with @ember/test-helpers
// visit https://www.npmjs.com/package/@ember/test-waiters for more info.
const response = await waitForPromise(
fetch(authEndpointURL, { method: 'post', body }),
);
const json = await response.json();
return response.status < 400
? resolve(this.normalizeData(json, login_name))
+6 -6
View File
@@ -21,9 +21,8 @@
"doc:toc": "doctoc README.md"
},
"dependencies": {
"ember-cli-babel": "^7.26.11",
"ember-cli-babel": "^8.2.0",
"ember-cli-htmlbars": "^6.2.0",
"ember-fetch": "^8.1.1",
"ember-simple-auth": "^4.2.2"
},
"devDependencies": {
@@ -31,7 +30,8 @@
"@babel/plugin-proposal-decorators": "^7.21.0",
"@ember/optional-features": "^2.0.0",
"@ember/string": "^3.0.1",
"@ember/test-helpers": "^2.9.3",
"@ember/test-helpers": "^3.3.1",
"@ember/test-waiters": "^3.1.0",
"@embroider/test-setup": "^2.1.1",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
@@ -40,7 +40,7 @@
"doctoc": "^2.2.0",
"ember-auto-import": "^2.6.3",
"ember-cli": "^4.12.2",
"ember-cli-dependency-checker": "^3.3.1",
"ember-cli-dependency-checker": "^3.3.2",
"ember-cli-inject-live-reload": "^2.1.0",
"ember-cli-sri": "^2.1.1",
"ember-cli-terser": "^4.0.2",
@@ -49,7 +49,7 @@
"ember-export-application-global": "^2.0.1",
"ember-load-initializers": "^2.1.2",
"ember-page-title": "^7.0.0",
"ember-qunit": "^6.2.0",
"ember-qunit": "^8.1.0",
"ember-resolver": "^10.0.0",
"ember-source": "~4.12.0",
"ember-source-channel-url": "^3.0.0",
@@ -64,7 +64,7 @@
"prettier": "^3.0.0",
"qunit": "^2.19.4",
"qunit-dom": "^2.0.0",
"webpack": "^5.78.0"
"webpack": "^5.94.0"
},
"engines": {
"node": "18.* || 20.*"
+1 -1
View File
@@ -28,7 +28,7 @@ function setupApplicationTest(hooks, options) {
// This is also a good place to call test setup functions coming
// from other addons:
//
// setupIntl(hooks); // ember-intl
// setupIntl(hooks, 'en-us'); // ember-intl
// setupMirage(hooks); // ember-cli-mirage
}
@@ -3,17 +3,10 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Button
...attributes
@style='inline-link-action'
@iconLeft={{if
this.isLoading
'flight-icons/svg/loading-16'
'flight-icons/svg/reload-16'
}}
class={{if this.isLoading 'loading-button is-loading' 'loading-button'}}
<Hds::Button
@text={{t 'actions.refresh'}}
@color='secondary'
@icon={{if this.isLoading 'loading' 'reload'}}
@disabled={{this.isLoading}}
{{on 'click' this.toggleRefresh}}
>
{{yield}}
</Rose::Button>
/>
@@ -3,12 +3,17 @@
SPDX-License-Identifier: BUSL-1.1
}}
{{#if this.lastRefreshed}}
<Rose::Toolbar::Info>
{{relative-datetime-live this.lastRefreshed}}
</Rose::Toolbar::Info>
{{/if}}
<LoadingButton @onClick={{this.onClick}}>
{{t 'actions.refresh'}}
</LoadingButton>
<div class='toolbar-refresher'>
{{#if (has-block)}}
{{yield}}
{{else}}
{{#if this.lastRefreshed}}
<Hds::Text::Body @tag='p' @color='faint'>
{{relative-datetime-live this.lastRefreshed}}
</Hds::Text::Body>
{{/if}}
{{/if}}
<LoadingButton @onClick={{this.onClick}}>
{{t 'actions.refresh'}}
</LoadingButton>
</div>
@@ -15,7 +15,7 @@ export default class ToolbarRefresherComponent extends Component {
// =methods
/**
* Executes the click handler argument and trackes the time it completed.
* Executes the click handler argument and tracks the time it completed.
*/
@action
async onClick() {
@@ -5,7 +5,8 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import DurationUnitFormat from 'intl-unofficial-duration-unit-format';
import { DurationFormat } from '@formatjs/intl-durationformat';
import { secondsToDuration } from 'core/utils/seconds-to-duration';
export default class extends Helper {
// =services
@@ -19,12 +20,10 @@ export default class extends Helper {
*/
compute([durationInMs]) {
const durationInSeconds = durationInMs / 1000;
const durationFormat = new DurationUnitFormat(this.intl.primaryLocale, {
const durationFormat = new DurationFormat(this.intl.primaryLocale, {
style: 'narrow',
format: '{days} {hours} {minutes} {seconds}',
round: true,
});
return durationFormat.format(durationInSeconds);
return durationFormat.format(secondsToDuration(durationInSeconds));
}
}
+8 -8
View File
@@ -5,7 +5,8 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import DurationUnitFormat from 'intl-unofficial-duration-unit-format';
import { DurationFormat } from '@formatjs/intl-durationformat';
import { secondsToDuration } from 'core/utils/seconds-to-duration';
export default class extends Helper {
// =services
@@ -29,14 +30,13 @@ export default class extends Helper {
difference > 0 ? Math.floor(difference / 1000) : 0;
// Format the duration
const duration = new DurationUnitFormat(this.intl.primaryLocale, {
style: 'timer',
format: `{hours}:{minutes}:{seconds} ${this.intl.t(
'resources.session.remaining',
)}`,
const duration = new DurationFormat(this.intl.primaryLocale, {
style: 'digital',
});
const formattedTime = duration.format(
secondsToDuration(differenceInSeconds),
);
// Return the formatted duration
return duration.format(differenceInSeconds);
return `${formattedTime} ${this.intl.t('resources.session.remaining')}`;
}
}
+16 -18
View File
@@ -7,7 +7,6 @@
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 1rem;
gap: 0.5rem;
.filter-tag {
@@ -27,33 +26,20 @@
margin-top: 3rem;
}
.hds-segmented-group {
> div,
table {
margin-bottom: 1rem;
}
.hds-segmented-group {
.hds-dropdown-toggle-button__text {
white-space: nowrap;
}
}
.hds-table {
margin-bottom: 1rem;
}
.search-filtering-toolbar {
display: flex;
// TODO: Remove these copied styles by migrating to HDS for the toolbar refresher component.
// The original styles assumed we were using the old toolbar component under a "rose-toolbar" class.
.rose-button-inline-link-action {
font-size: 0.8125rem;
line-height: 1.8461538462;
font-weight: 600;
color: var(--stark);
display: inline-block;
vertical-align: middle;
padding: 0.375rem 1rem;
}
> :last-child {
margin-left: auto;
}
@@ -67,3 +53,15 @@
color: var(--token-color-foreground-primary);
}
}
.toolbar-refresher {
display: flex;
align-items: center;
padding-left: 1rem;
gap: 1rem;
> .hds-text {
white-space: nowrap;
overflow: hidden;
}
}
@@ -0,0 +1,27 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
/**
* Takes in the number of seconds and converts to an object with corresponding
* values for each time duration. For example:
* { weeks: 0, days: 0, hours: 22, minutes: 15, seconds: 11 }
* @param {number} value
* @returns {object}
*/
export const secondsToDuration = (value) => {
const second = 1;
const minute = second * 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;
const weeks = Math.max(Math.floor(value / week), 0);
const days = Math.max(Math.floor((value % week) / day), 0);
const hours = Math.max(Math.floor((value % day) / hour), 0);
const minutes = Math.max(Math.floor((value % hour) / minute), 0);
const seconds = Math.max(Math.floor((value % minute) / second), 0);
return { weeks, days, hours, minutes, seconds };
};
@@ -3,4 +3,4 @@
* SPDX-License-Identifier: BUSL-1.1
*/
export { default, initialize } from 'api/initializers/deprecations';
export { default } from 'core/utils/seconds-to-duration';
+8 -8
View File
@@ -31,19 +31,19 @@
"doc:toc": "doctoc README.md"
},
"dependencies": {
"@formatjs/intl-durationformat": "^0.2.4",
"api": "*",
"auth": "*",
"ember-auto-import": "^2.6.3",
"ember-browser-services": "^4.0.3",
"ember-cli-babel": "^7.26.11",
"ember-cli-babel": "^8.2.0",
"ember-cli-htmlbars": "^6.2.0",
"ember-cli-sass": "^11.0.1",
"ember-feature-flags": "^6.0.0",
"ember-intl": "^6.0.0-beta.6",
"ember-intl": "^7.0.5",
"ember-loading": "^2.0.0",
"ember-truth-helpers": "^3.0.0",
"filesize": "^10.0.7",
"intl-unofficial-duration-unit-format": "^3.1.0",
"sass": "^1.69.5"
},
"devDependencies": {
@@ -51,7 +51,7 @@
"@babel/plugin-proposal-decorators": "^7.21.0",
"@ember/optional-features": "^2.0.0",
"@ember/string": "^3.0.1",
"@ember/test-helpers": "^2.9.3",
"@ember/test-helpers": "^3.3.1",
"@embroider/test-setup": "^2.1.1",
"@faker-js/faker": "^8.0.2",
"@glimmer/component": "^1.1.2",
@@ -60,14 +60,14 @@
"concurrently": "^8.0.1",
"doctoc": "^2.2.0",
"ember-cli": "^4.12.2",
"ember-cli-dependency-checker": "^3.3.1",
"ember-cli-dependency-checker": "^3.3.2",
"ember-cli-inject-live-reload": "^2.1.0",
"ember-cli-sri": "^2.1.1",
"ember-cli-terser": "^4.0.2",
"ember-inline-svg": "^1.0.1",
"ember-load-initializers": "^2.1.2",
"ember-page-title": "^7.0.0",
"ember-qunit": "^6.2.0",
"ember-qunit": "^8.1.0",
"ember-resolver": "^10.0.0",
"ember-source": "~4.12.0",
"ember-source-channel-url": "^3.0.0",
@@ -84,8 +84,8 @@
"qunit": "^2.19.4",
"qunit-dom": "^2.0.0",
"rose": "*",
"sinon": "^17.0.1",
"webpack": "^5.78.0"
"sinon": "^18.0.0",
"webpack": "^5.94.0"
},
"peerDependencies": {
"ember-source": "^4.0.0",
+1 -1
View File
@@ -28,7 +28,7 @@ function setupApplicationTest(hooks, options) {
// This is also a good place to call test setup functions coming
// from other addons:
//
// setupIntl(hooks); // ember-intl
// setupIntl(hooks, 'en-us'); // ember-intl
// setupMirage(hooks); // ember-cli-mirage
}
@@ -11,7 +11,7 @@ import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | dropdown/index', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
setupIntl(hooks, 'en-us');
const firstItem = {
id: '1',
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
module('Integration | Component | error/message', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
setupIntl(hooks, 'en-us');
test('it renders predefined message for known error status', async function (assert) {
await render(hbs`<Error::Message @status='401' />`);
@@ -11,7 +11,7 @@ import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | filter-tags/index', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
setupIntl(hooks, 'en-us');
test('it renders', async function (assert) {
this.set('filter', {
@@ -16,7 +16,7 @@ import {
module('Integration | Component | form | authenticate', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
setupIntl(hooks, 'en-us');
test('it shows login name and password when LDAP is selected from the list', async function (assert) {
const store = this.owner.lookup('service:store');
@@ -7,19 +7,22 @@ import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupIntl } from 'ember-intl/test-support';
module('Integration | Component | loading-button', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks, 'en-us');
test('it renders', async function (assert) {
await render(hbs`<LoadingButton />`);
assert.ok(find('.loading-button'));
assert.ok(find('.hds-button'));
});
test('it executes a function on refresh button click', async function (assert) {
assert.expect(2);
this.onClick = () => assert.ok(true, 'refresh was clicked');
await render(hbs`<LoadingButton @onClick={{this.onClick}} />`);
assert.ok(find('.loading-button'));
assert.ok(find('.hds-button'));
await click('button');
});
});
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
module('Integration | Helper | format-day-year', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
setupIntl(hooks, 'en-us');
test('it renders correctly when it is 1 year', async function (assert) {
this.set('inputValue', 365);
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
module('Integration | Helper | format-time-duration', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
setupIntl(hooks, 'en-us');
test('It renders in human-readable duration strings given different durations in milliseconds', async function (assert) {
// Set mock input (in milliseconds) durations
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
module('Integration | Helper | relative-datetime-live', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
setupIntl(hooks, 'en-us');
let now;
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
module('Integration | Helper | reverse-indexed-display-name', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
setupIntl(hooks, 'en-us');
test('it renders the resource name with its position in parent array reversed', async function (assert) {
this.set('array', ['Boundary', 'is', 'cool!']);
@@ -11,11 +11,9 @@ import { setupIntl } from 'ember-intl/test-support';
module('Integration | Helper | time-remaining', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
test('it calculates the correct time remaining', async function (assert) {
assert.expect(1);
setupIntl(hooks, 'en-us');
test('it calculates the correct time remainin with less than 24 hours', async function (assert) {
this.set('expirationTime', new Date(Date.now() + 1000 * 60 * 60 * 24));
await render(hbs`{{time-remaining this.expirationTime}}`);
@@ -23,9 +21,29 @@ module('Integration | Helper | time-remaining', function (hooks) {
assert.strictEqual(this.element.textContent.trim(), '23:59:59 remaining');
});
test('it handles negative time remaining', async function (assert) {
assert.expect(1);
test('it calculates the correct time remaining with more than 24 hours', async function (assert) {
this.set('expirationTime', new Date(Date.now() + 1001 * 60 * 60 * 24));
await render(hbs`{{time-remaining this.expirationTime}}`);
assert.strictEqual(
this.element.textContent.trim(),
'1 day, 0:01:26 remaining',
);
});
test('it calculates the correct time remaining with more than a week', async function (assert) {
this.set('expirationTime', new Date(Date.now() + 1001 * 60 * 60 * 24 * 8));
await render(hbs`{{time-remaining this.expirationTime}}`);
assert.strictEqual(
this.element.textContent.trim(),
'1 wk, 1 day, 0:11:31 remaining',
);
});
test('it handles negative time remaining', async function (assert) {
this.set('expirationTime', new Date(Date.now() - 1000 * 60 * 60 * 24));
await render(hbs`{{time-remaining this.expirationTime}}`);
@@ -22,7 +22,7 @@ const listWithOneItem = [{ key: 1, value: 'boundary' }];
const limit = 3;
module('Integration | Helper | truncate-list', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
setupIntl(hooks, 'en-us');
test('it renders correctly when there is list with more than the limit specificed', async function (assert) {
this.set('inputValue', list);
@@ -0,0 +1,79 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { secondsToDuration } from 'core/utils/seconds-to-duration';
import { module, test } from 'qunit';
module('Unit | Utility | seconds-to-duration', function () {
test('it handles 0 correctly', function (assert) {
const result = secondsToDuration(0);
assert.strictEqual(result.seconds, 0);
assert.strictEqual(result.minutes, 0);
assert.strictEqual(result.hours, 0);
assert.strictEqual(result.days, 0);
assert.strictEqual(result.weeks, 0);
});
test('it converts seconds correctly', function (assert) {
const result = secondsToDuration(45);
assert.strictEqual(result.seconds, 45);
assert.strictEqual(result.minutes, 0);
assert.strictEqual(result.hours, 0);
assert.strictEqual(result.days, 0);
assert.strictEqual(result.weeks, 0);
});
test('it converts minutes correctly', function (assert) {
const result = secondsToDuration(100);
assert.strictEqual(result.seconds, 40);
assert.strictEqual(result.minutes, 1);
assert.strictEqual(result.hours, 0);
assert.strictEqual(result.days, 0);
assert.strictEqual(result.weeks, 0);
});
test('it converts hours correctly', function (assert) {
const result = secondsToDuration(5000);
assert.strictEqual(result.seconds, 20);
assert.strictEqual(result.minutes, 23);
assert.strictEqual(result.hours, 1);
assert.strictEqual(result.days, 0);
assert.strictEqual(result.weeks, 0);
});
test('it converts days correctly', function (assert) {
const result = secondsToDuration(500000);
assert.strictEqual(result.seconds, 20);
assert.strictEqual(result.minutes, 53);
assert.strictEqual(result.hours, 18);
assert.strictEqual(result.days, 5);
assert.strictEqual(result.weeks, 0);
});
test('it converts weeks correctly', function (assert) {
const result = secondsToDuration(800000);
assert.strictEqual(result.seconds, 20);
assert.strictEqual(result.minutes, 13);
assert.strictEqual(result.hours, 6);
assert.strictEqual(result.days, 2);
assert.strictEqual(result.weeks, 1);
});
test('it handles negative values', function (assert) {
const result = secondsToDuration(-800000);
assert.strictEqual(result.seconds, 0);
assert.strictEqual(result.minutes, 0);
assert.strictEqual(result.hours, 0);
assert.strictEqual(result.days, 0);
assert.strictEqual(result.weeks, 0);
});
});
@@ -22,6 +22,8 @@ back: Back
view: View
retry: Retry
play: Play
pause: Pause
resume: Resume
get-topic-help: Get help for this topic
copy-to-clipboard: Copy to Clipboard
copy-error-detail-to-clipboard: Copy error detail to clipboard
@@ -60,3 +62,5 @@ yes: Yes
no: No
narrow-results: Narrow results
overflow-options: Overflow Options
show-errors: Show Errors
hide-errors: Hide Errors
+28
View File
@@ -105,3 +105,31 @@ unsupported-controller:
releases-page: Go to releases page
change-cluster-url: Change cluster URL
error: Unable to download supported version of desktop app. You’ll need to download version 1.7.1 from the releases page
settings:
application: Application
cli: CLI
cache-daemon: Cache Daemon
client-agent:
title: Boundary Client Agent
description: Required to use Transparent Sessions
client: Client
version: Version
color-theme: Color Theme
provider: Provider
hcp: HashiCorp Cloud Platform
self-managed: Self-managed
server:
title: Server
description: Boundary connects you to targets using this server managed by your organization.
logs:
title: Logs
logging-level: Logging level
log-location: Log location
levels:
error: Error
warn: Warn
info: Info
debug: Debug
alerts:
cache-daemon: There may be a problem with the cache daemon
client-agent: There may be a problem with the client agent
+39 -7
View File
@@ -212,12 +212,25 @@ host-catalog:
help:
static: Provide a list of hosts and host sets with static addresses
plugin: Automate discovery of target hosts and services on Azure or AWS
role_arn: The ARN (Amazon Resource Name) used by this instance.
disable_credential_rotation: Credential rotation is automatically disabled when you use dynamic credentials.
types:
static: Static
unknown: Plugin
aws: AWS
azure: Azure
plugin: Dynamic
credential:
label: Credential type
help: Select the credential type that you want to use.
static-credential:
edit: Static Credentials
label: Use an access key (Static Credentials)
help: Use an access key that you have generated in AWS to use with this AWS Host Catalog.
dynamic-credential:
edit: Dynamic Credentials
label: Use Assume Role (Dynamic Credentials)
help: Specify a role used to generate credentials to use with this AWS Host Catalog.
form:
aws_region:
label: AWS Region
@@ -225,6 +238,9 @@ host-catalog:
azure_client_id:
label: Client/Application ID
help: The client (application) ID of an Azure service principal that Boundary will use to authenticate and discover hosts from Azure.
worker_filter:
label: Worker Filter
help: Insert a worker filter in order to route requests to the correct worker.
host-set:
title: Host Set
title_plural: Host Sets
@@ -357,6 +373,12 @@ session-recording:
duration: '{time} duration'
questions:
delete: Are you sure you want to delete this recording?
filters:
time:
title: Time
last-twenty-four-hours: Last 24 hours
last-three-days: Last 3 days
last-seven-days: Last 7 days
connection:
title: Connection
title_index: Connection {index}
@@ -384,9 +406,9 @@ session-recording:
messages:
none:
title: Channel Playback
missing:
title: We can't play back this channel because the file is missing
description: This likely means the connected storage bucket is misconfigured or the file was deleted.
error:
title: Playback error
description: This may mean the connected storage bucket is misconfigured, the file was deleted, or integrity validation failed.
link: Retrieving lost session recordings
not-supported:
title: Playback is not supported for this channel
@@ -439,6 +461,7 @@ storage-bucket:
plugin-types:
aws: Amazon S3
minio: MinIO
unknown: Unknown
form:
scope:
label: Scope
@@ -447,7 +470,6 @@ storage-bucket:
help_org: This storage bucket will be available for use with targets within this org.
provider:
label: Provider
help: Boundary currently only supports Amazon S3 storage.
endpoint_url:
label: Endpoint URL
help: Cannot be changed later.
@@ -459,7 +481,7 @@ storage-bucket:
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 (Amazon Resource 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.
@@ -615,6 +637,10 @@ role:
help: Customize which scopes are associated with this role.
view-projects: '{selected}/{total} total projects'
apply: Apply changes to org
remove-org-and-projects: Remove org and projects
remove-org-only: Remove just the org
remove-orgs-and-projects: Remove orgs and projects
remove-orgs-only: Remove just the orgs
form:
this:
label: Add this scope
@@ -627,6 +653,12 @@ role:
descendants:
label: Add all descendants
help: Add all of the orgs and the projects in the orgs that are underneath this scope.
remove-org:
title: Remove this org?
description: You're about to remove {orgDisplayName}. Do you want to remove the projects under {orgDisplayName} as well? Or do you just want to remove {orgDisplayName} from this project?
remove-all-orgs:
title: Remove all selected orgs?
description: You're about to remove all the selected orgs. Do you want to remove the projects under these orgs as well? Or do you just want to remove just the orgs?
target:
title: Target
title_plural: Targets
@@ -689,7 +721,7 @@ target:
help: The local proxy port on which to listen by default when a session is started on a client.
sidebar:
label: Session Recording
help: Record SSH sessions on this target and save them to an Amazon S3 bucket.
help: Record SSH sessions on this target and save them to a storage bucket.
link: Session Recording settings
questions:
delete-host-sources:
@@ -873,7 +905,7 @@ credential-library:
help: The body of the HTTP request the library sends to Vault when requesting credentials. Only allowed when HTTP method is set to POST.
username:
label: Username
help: The username to use when making an SSH connection. This can be templated to use the requesting user's name. This will be included in the valid_principles when making the request to Vault.
help: The username to use when making an SSH connection. This can be templated to use the requesting user's name. This will be included in the valid_principals when making the request to Vault.
key_type:
label: Key Type
help: Specifies the desired key type to use when generating a private key.
@@ -14,7 +14,18 @@ active-public: Public
recording: Recording
completed: Completed
failed: Failed
installed: Installed
not-running: Not Running
running: Running
paused: Paused
authenticated: Authenticated
unauthenticated: Unauthenticated
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.
incomplete-loading:
limit: Loaded the first {resultCount} results. Please narrow down your search or filters if the result you're looking for is not shown.
refreshing:
description: Updating cache...
tooltip: Some items may not appear until the cache is finished updating
+7 -7
View File
@@ -33,7 +33,7 @@
"@hashicorp/design-system-components": "^4.4.1",
"codemirror": "5.65.7",
"ember-auto-import": "^2.6.3",
"ember-cli-babel": "^7.26.11",
"ember-cli-babel": "^8.2.0",
"ember-cli-clipboard": "^1.0.0",
"ember-cli-htmlbars": "^6.2.0",
"ember-cli-sass": "^11.0.1",
@@ -46,12 +46,12 @@
"sass": "^1.69.5"
},
"devDependencies": {
"@babel/core": "^7.18.5",
"@babel/core": "^7.25.2",
"@babel/eslint-parser": "^7.21.3",
"@babel/plugin-proposal-decorators": "^7.21.0",
"@ember/optional-features": "^2.0.0",
"@ember/string": "^3.0.1",
"@ember/test-helpers": "^2.9.3",
"@ember/test-helpers": "^3.3.1",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
"babel-loader": "^8.2.5",
@@ -61,7 +61,7 @@
"concurrently": "^8.0.1",
"doctoc": "^2.2.0",
"ember-cli": "^4.12.2",
"ember-cli-dependency-checker": "^3.3.1",
"ember-cli-dependency-checker": "^3.3.2",
"ember-cli-inject-live-reload": "^2.1.0",
"ember-cli-sri": "^2.1.1",
"ember-cli-terser": "^4.0.2",
@@ -73,7 +73,7 @@
"ember-maybe-import-regenerator": "^1.0.0",
"ember-modifier": "^3.2.7",
"ember-page-title": "^7.0.0",
"ember-qunit": "^6.2.0",
"ember-qunit": "^8.1.0",
"ember-resolver": "^10.0.0",
"ember-source": "~4.12.0",
"ember-source-channel-url": "^3.0.0",
@@ -84,7 +84,7 @@
"eslint-plugin-ember": "^11.11.1",
"eslint-plugin-n": "^16.3.1",
"eslint-plugin-qunit": "^8.0.1",
"jest": "^27.4.7",
"jest": "^29.7.0",
"loader.js": "^4.7.0",
"postcss": "^8.4.31",
"prettier": "^3.0.0",
@@ -96,7 +96,7 @@
"stylelint": "^15.10.1",
"stylelint-config-prettier-scss": "^0.0.1",
"stylelint-config-standard-scss": "^11.0.0",
"webpack": "^5.78.0"
"webpack": "^5.94.0"
},
"peerDependencies": {
"ember-source": "^4.0.0"
+9 -6
View File
@@ -38,21 +38,22 @@
"test:ui-desktop": "yarn --cwd ui/desktop test",
"compliance:licenses": "license-checker --onlyAllow 'Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;BUSL-1.1;Public Domain;Python-2.0;Unicode-TOU;Unlicense;WTFPL' --excludePackages 'boundary-ui;admin@0.0.0;desktop@0.0.0;base64-js@0.0.2;cycle@1.0.3;cldr-core@43.1.0;make-plural@7.2.0;@hashicorp/ember-asciinema-player@0.0.0;path-scurry@1.10.1'",
"doc:toc": "doctoc README.md",
"prepare": "husky install"
"prepare": "husky install",
"postinstall": "patch-package"
},
"devDependencies": {
"cz-conventional-changelog": "^3.1.0",
"doctoc": "^2.2.0",
"git-cz": "^4.8.0",
"git-cz": "^4.9.0",
"husky": "^8.0.0",
"license-checker": "^25.0.1",
"lint-staged": "^13.0.2"
"lint-staged": "^13.0.2",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0"
},
"resolutions": {
"request": "^2.88.0",
"loader-utils": "^2.0.4",
"**/ember-electron/**/minimatch": "^3.0.5",
"**/babel-core/json5": "^2.2.2",
"**/react-syntax-highlighter/refractor/prismjs": "^1.27.0",
"**/nomnom/underscore": "^1.12.1",
"**/remark-parse/trim": "^1.0.1",
@@ -72,7 +73,9 @@
"**/ember-cli/**/ansi-html": "^0.0.9",
"**/ember-cli/testem/fireworm/async": "^2.6.4",
"**/desktop/@doyensec/electronegativity/winston/async": "^2.6.4",
"**/@hashicorp/design-system-components/ember-stargate": "^0.6.0"
"**/@hashicorp/design-system-components/ember-stargate": "^0.6.0",
"micromatch": "^4.0.8",
"send": "^0.19.0"
},
"config": {
"commitizen": {
+45
View File
@@ -0,0 +1,45 @@
diff --git a/node_modules/lodash.template/index.js b/node_modules/lodash.template/index.js
index f051141..0c45a8d 100644
--- a/node_modules/lodash.template/index.js
+++ b/node_modules/lodash.template/index.js
@@ -17,6 +17,9 @@ var HOT_COUNT = 800,
var INFINITY = 1 / 0,
MAX_SAFE_INTEGER = 9007199254740991;
+/** Error message constants. */
+var INVALID_TEMPL_VAR_ERROR_TEXT = 'Invalid `variable` option passed into `_.template`';
+
/** `Object#toString` result references. */
var argsTag = '[object Arguments]',
arrayTag = '[object Array]',
@@ -1343,6 +1346,18 @@ function keysIn(object) {
return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object);
}
+/**
+ * Used to validate the `validate` option in `_.template` variable.
+ *
+ * Forbids characters which could potentially change the meaning of the function argument definition:
+ * - "()," (modification of function parameters)
+ * - "=" (default value)
+ * - "[]{}" (destructuring of function parameters)
+ * - "/" (beginning of a comment)
+ * - whitespace
+ */
+var reForbiddenIdentifierChars = /[()=,{}\[\]\/\s]/;
+
/**
* Creates a compiled template function that can interpolate data properties
* in "interpolate" delimiters, HTML-escape interpolated data properties in
@@ -1522,6 +1537,11 @@ function template(string, options, guard) {
if (!variable) {
source = 'with (obj) {\n' + source + '\n}\n';
}
+ // Throw an error if a forbidden character was found in `variable`, to prevent
+ // potential command injection attacks.
+ else if (reForbiddenIdentifierChars.test(variable)) {
+ throw new Error(INVALID_TEMPL_VAR_ERROR_TEXT);
+ }
// Cleanup code by stripping empty strings.
source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source)
.replace(reEmptyStringMiddle, '$1')
+14
View File
@@ -64,5 +64,19 @@ module.exports = {
'qunit/require-expect': [2, 'except-simple'],
},
},
{
// e2e files
files: ['tests/e2e/**'],
env: {
node: true,
},
extends: ['eslint:recommended', 'prettier'],
rules: {
'no-empty-pattern': [
'error',
{ allowObjectPatternsAsParameters: true },
],
},
},
],
};
+3 -3
View File
@@ -1,6 +1,6 @@
# Admin UI
The admin UI for Boundary.
The Admin UI for Boundary.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
@@ -39,8 +39,8 @@ List of available project commands. `yarn run <command-name>`
| Command | Description |
| ------- | ----------- |
| build:development | Builds the UI in development mode. |
| build | Builds the UI for production. |
| build:development | Builds the Admin UI in development mode. |
| build | Builds the Admin UI for production. |
| lint | Runs all lint commands. |
| lint:hbs | Lints `hbs` template files. |
| lint:js | Lints `js` files. |
@@ -4,17 +4,12 @@
}}
{{#if (can 'delete account' @model)}}
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
as |dropdown|
>
<dropdown.button
@style='danger'
@disabled={{@model.canSave}}
<Hds::Dropdown data-test-manage-account-auth-methods as |dd|>
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
<dd.Interactive
@text={{t 'resources.account.actions.delete'}}
@color='critical'
{{on 'click' (fn @delete @model)}}
>
{{t 'resources.account.actions.delete'}}
</dropdown.button>
</Rose::Dropdown>
/>
</Hds::Dropdown>
{{/if}}
@@ -40,55 +40,42 @@
</Rose::Dropdown>
{{/if}}
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
as |dropdown|
>
<Hds::Dropdown data-test-manage-auth-methods-dropdown as |dd|>
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
{{#if @model.isPrimary}}
<dropdown.button
@disabled={{@model.canSave}}
<dd.Interactive
@text={{t 'resources.auth-method.actions.remove-as-primary'}}
{{on 'click' (fn @removeAsPrimary @model)}}
>
{{t 'resources.auth-method.actions.remove-as-primary'}}
</dropdown.button>
/>
{{else}}
<dropdown.button
@disabled={{@model.canSave}}
<dd.Interactive
@text={{t 'resources.auth-method.actions.make-primary'}}
{{on 'click' (fn @makePrimary @model)}}
>
{{t 'resources.auth-method.actions.make-primary'}}
</dropdown.button>
/>
{{/if}}
{{#if (can 'create model' @model collection='accounts')}}
<dropdown.link
<dd.Interactive
@text={{t 'resources.account.actions.create'}}
@route='scopes.scope.auth-methods.auth-method.accounts.new'
@model={{@model}}
>
{{t 'resources.account.actions.create'}}
</dropdown.link>
/>
{{/if}}
{{#if (can 'create model' @model collection='managed-groups')}}
{{#if (or @model.isOIDC @model.isLDAP)}}
<dropdown.link
<dd.Interactive
@text={{t 'resources.managed-group.actions.create'}}
@route='scopes.scope.auth-methods.auth-method.managed-groups.new'
@model={{@model}}
>
{{t 'resources.managed-group.actions.create'}}
</dropdown.link>
/>
{{/if}}
{{/if}}
{{#if (can 'delete auth-method' @model)}}
<dropdown.separator />
<dropdown.button
@style='danger'
@disabled={{@model.canSave}}
<dd.Separator />
<dd.Interactive
@text={{t 'resources.auth-method.actions.delete'}}
@color='critical'
{{on 'click' (fn @delete @model)}}
>
{{t 'resources.auth-method.actions.delete'}}
</dropdown.button>
/>
{{/if}}
</Rose::Dropdown>
</Hds::Dropdown>
@@ -4,17 +4,12 @@
}}
{{#if (can 'delete model' @model)}}
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
as |dropdown|
>
<dropdown.button
@style='danger'
@disabled={{@model.canSave}}
<Hds::Dropdown data-test-managed-group-dropdown as |dd|>
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
<dd.Interactive
@text={{t 'resources.managed-group.actions.delete'}}
@color='critical'
{{on 'click' (fn @delete @model)}}
>
{{t 'resources.managed-group.actions.delete'}}
</dropdown.button>
</Rose::Dropdown>
/>
</Hds::Dropdown>
{{/if}}
@@ -2,37 +2,26 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
as |dropdown|
>
<Hds::Dropdown data-test-manage-credential-stores-dropdown as |dd|>
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
{{#if (can 'create model' @model collection='credential-libraries')}}
<dropdown.link
<dd.Interactive
@route='scopes.scope.credential-stores.credential-store.credential-libraries.new'
@model={{@model}}
>
{{t 'resources.credential-library.actions.create'}}
</dropdown.link>
@text={{t 'resources.credential-library.actions.create'}}
/>
{{/if}}
{{#if (can 'create model' @model collection='credentials')}}
<dropdown.link
<dd.Interactive
@route='scopes.scope.credential-stores.credential-store.credentials.new'
@model={{@model}}
>
{{t 'resources.credential.actions.create'}}
</dropdown.link>
@text={{t 'resources.credential.actions.create'}}
/>
{{/if}}
{{#if (can 'delete model' @model)}}
<dropdown.separator />
<dropdown.button
@style='danger'
@disabled={{@model.canSave}}
<dd.Separator />
<dd.Interactive
@text={{t 'resources.credential-store.actions.delete'}}
@color='critical'
{{on 'click' @delete}}
>
{{t 'resources.credential-store.actions.delete'}}
</dropdown.button>
/>
{{/if}}
</Rose::Dropdown>
</Hds::Dropdown>
@@ -19,12 +19,18 @@
{{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>
<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>
<E.Message>
{{error.message}}
</E.Message>
{{/each}}
</F.Error>
{{/if}}
@@ -38,21 +44,28 @@
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
<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>
<E.Message>
{{error.message}}
</E.Message>
{{/each}}
</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>
<G.Legend>
{{t 'form.type.label'}}
</G.Legend>
{{#each this.hostCatalogTypes as |hostCatalogType|}}
<G.RadioCard
@value={{hostCatalogType}}
@@ -61,19 +74,25 @@
{{on 'input' (fn @changeType hostCatalogType)}}
as |R|
>
<R.Label>{{t
(concat 'resources.host-catalog.types.' hostCatalogType)
}}</R.Label>
<R.Description>{{t
(concat 'resources.host-catalog.help.' hostCatalogType)
}}</R.Description>
<R.Label>
{{t (concat 'resources.host-catalog.types.' hostCatalogType)}}
</R.Label>
<R.Description>
{{t (concat 'resources.host-catalog.help.' hostCatalogType)}}
</R.Description>
</G.RadioCard>
{{/each}}
</Hds::Form::RadioCard::Group>
<Hds::Form::RadioCard::Group @name='plugin_type' @alignment='center' as |G|>
<G.Legend>{{t 'titles.provider'}}</G.Legend>
<G.Legend>{{t 'titles.choose-a-provider'}}</G.Legend>
<G.HelperText>{{t 'descriptions.choose-a-provider'}}</G.HelperText>
<G.Legend>
{{t 'titles.provider'}}
</G.Legend>
<G.Legend>
{{t 'titles.choose-a-provider'}}
</G.Legend>
<G.HelperText>
{{t 'descriptions.choose-a-provider'}}
</G.HelperText>
{{#each-in this.mapResourceTypeWithIcon as |pluginType icon|}}
<G.RadioCard
@@ -83,11 +102,10 @@
{{on 'input' (fn @changeType pluginType)}}
as |R|
>
<R.Label>{{t
(concat 'resources.host-catalog.types.' pluginType)
}}</R.Label>
<R.Label>
{{t (concat 'resources.host-catalog.types.' pluginType)}}
</R.Label>
<R.Icon @name={{icon}} />
</G.RadioCard>
{{/each-in}}
</Hds::Form::RadioCard::Group>
@@ -98,8 +116,11 @@
readonly={{true}}
as |F|
>
<F.Label>{{t 'titles.provider'}}</F.Label>
<F.HelperText>{{t 'descriptions.provider'}}
<F.Label>
{{t 'titles.provider'}}
</F.Label>
<F.HelperText>
{{t 'descriptions.provider'}}
</F.HelperText>
</InfoField>
{{/if}}
@@ -112,7 +133,9 @@
{{on 'input' (set-from-event @model 'region')}}
as |F|
>
<F.Label>{{t 'resources.host-catalog.form.aws_region.label'}}</F.Label>
<F.Label>
{{t 'resources.host-catalog.form.aws_region.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.host-catalog.form.aws_region.help'}}
<Hds::Link::Inline @href={{doc-url 'host-catalog.aws.region'}}>
@@ -121,50 +144,289 @@
</F.HelperText>
</Hds::Form::TextInput::Field>
<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|
>
<F.Label>{{t 'form.access_key_id.label'}}</F.Label>
<F.HelperText>
{{t 'form.access_key_id.help'}}
<Hds::Link::Inline @href={{doc-url 'host-catalog.aws'}}>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
</Hds::Form::TextInput::Field>
{{#if @model.isNew}}
<Hds::Form::Radio::Group
class='dynamic-credential-selection'
@layout='vertical'
@name='credential_type'
as |G|
>
<G.Legend>
{{t 'resources.host-catalog.types.credential.label'}}
</G.Legend>
<G.HelperText>
{{t 'resources.host-catalog.types.credential.help'}}
</G.HelperText>
<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|
>
<F.Label>{{t 'form.secret_access_key.label'}}</F.Label>
<F.HelperText>
{{t 'form.secret_access_key.help'}}
<Hds::Link::Inline @href={{doc-url 'host-catalog.aws'}}>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
</Hds::Form::TextInput::Field>
{{#each this.credentials as |credentialType|}}
<G.RadioField
name={{credentialType}}
@value={{credentialType}}
checked={{eq credentialType this.selectedCredentialType}}
disabled={{form.disabled}}
{{on 'input' (fn this.updateCredentialTypeSelection credentialType)}}
as |F|
>
<F.Label>
{{t
(concat
'resources.host-catalog.types.credential.'
credentialType
'.label'
)
}}
</F.Label>
<F.HelperText>
{{t
(concat
'resources.host-catalog.types.credential.'
credentialType
'.help'
)
}}
</F.HelperText>
</G.RadioField>
{{/each}}
</Hds::Form::Radio::Group>
{{else}}
<Hds::Form::Fieldset @layout='horizontal' as |F|>
<F.Legend>
{{t 'resources.host-catalog.types.credential.label'}}
</F.Legend>
<F.Control>
<Hds::Badge
@text={{t
(concat
'resources.host-catalog.types.credential.'
this.selectedCredentialType
'.edit'
)
}}
/>
</F.Control>
</Hds::Form::Fieldset>
{{/if}}
<Hds::Form::Checkbox::Field
name='disable_credential_rotation'
checked={{@model.disable_credential_rotation}}
disabled={{form.disabled}}
{{on 'change' (fn this.toggleDisableCredentialRotation @model)}}
as |F|
>
<F.Label>{{t 'form.disable_credential_rotation.label'}}</F.Label>
</Hds::Form::Checkbox::Field>
{{#if this.showDynamicCredentials}}
{{! role_arn }}
<Hds::Form::TextInput::Field
name='role_arn'
@isRequired={{true}}
@value={{@model.role_arn}}
@isInvalid={{@model.errors.role_arn}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'role_arn')}}
as |F|
>
<F.Label>
{{t 'resources.storage-bucket.form.role_arn.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.host-catalog.help.role_arn'}}
</F.HelperText>
{{#if @model.errors.role_arn}}
<F.Error as |E|>
{{#each @model.errors.role_arn as |error|}}
<E.Message>
{{error.message}}
</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! role_external_id }}
<Hds::Form::TextInput::Field
name='role_external_id'
@isOptional={{true}}
@value={{@model.role_external_id}}
@isInvalid={{@model.errors.role_external_id}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'role_external_id')}}
as |F|
>
<F.Label>
{{t 'resources.storage-bucket.form.role_external_id.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_external_id.help'}}
</F.HelperText>
{{#if @model.errors.role_external_id}}
<F.Error as |E|>
{{#each @model.errors.role_external_id as |error|}}
<E.Message>
{{error.message}}
</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! role_session_name }}
<Hds::Form::TextInput::Field
name='role_session_name'
@isOptional={{true}}
@value={{@model.role_session_name}}
@isInvalid={{@model.errors.role_session_name}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'role_session_name')}}
as |F|
>
<F.Label>
{{t 'resources.storage-bucket.form.role_session_name.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_session_name.help'}}
</F.HelperText>
{{#if @model.errors.role_session_name}}
<F.Error as |E|>
{{#each @model.errors.role_session_name as |error|}}
<E.Message>
{{error.message}}
</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! role_tags }}
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.storage-bucket.form.role_tags.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_tags.help'}}
</F.HelperText>
{{#if @model.errors.role_tags}}
<F.Error as |E|>
{{#each @model.errors.role_tags as |error|}}
<E.Message>
{{error.message}}
</E.Message>
{{/each}}
</F.Error>
{{/if}}
</:fieldset>
<:field as |F|>
<F.KeyValue
@name='role_tags'
@options={{@model.role_tags}}
@model={{@model}}
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
{{else}}
<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|
>
<F.Label>
{{t 'form.access_key_id.label'}}
</F.Label>
<F.HelperText>
{{t 'form.access_key_id.help'}}
<Hds::Link::Inline @href={{doc-url 'host-catalog.aws'}}>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
</Hds::Form::TextInput::Field>
<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|
>
<F.Label>
{{t 'form.secret_access_key.label'}}
</F.Label>
<F.HelperText>
{{t 'form.secret_access_key.help'}}
<Hds::Link::Inline @href={{doc-url 'host-catalog.aws'}}>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
</Hds::Form::TextInput::Field>
{{/if}}
{{#if (feature-flag 'dynamic-credentials-worker-filter')}}
<Hds::Form::Textarea::Field
data-test-dynamic-credential-worker-filter
name='worker_filter'
@value={{@model.worker_filter}}
@isOptional={{true}}
@isInvalid={{@model.errors.worker_filter}}
disabled={{form.disabled}}
as |F|
>
<F.Label>
{{t 'resources.host-catalog.form.worker_filter.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.host-catalog.form.worker_filter.help'}}
<Hds::Link::Inline @href={{doc-url 'worker.create-tags'}}>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
{{#if @model.errors.worker_filter}}
<F.Error as |E|>
{{#each @model.errors.worker_filter as |error|}}
<E.Message>
{{error.message}}
</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
{{/if}}
{{#if this.showDynamicCredentials}}
<Hds::Form::Checkbox::Field
name='disable_credential_rotation'
checked={{true}}
disabled={{true}}
as |F|
>
<F.Label>
{{t 'form.disable_credential_rotation.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.host-catalog.help.disable_credential_rotation'}}
</F.HelperText>
</Hds::Form::Checkbox::Field>
{{else}}
<Hds::Form::Checkbox::Field
name='disable_credential_rotation'
checked={{@model.disable_credential_rotation}}
disabled={{form.disabled}}
{{on 'change' (fn this.toggleDisableCredentialRotation @model)}}
as |F|
>
<F.Label>
{{t 'form.disable_credential_rotation.label'}}
</F.Label>
</Hds::Form::Checkbox::Field>
{{/if}}
&nbsp;
{{#if (can 'save model' @model)}}
@@ -8,7 +8,10 @@ import { action } from '@ember/object';
import {
TYPES_HOST_CATALOG,
TYPES_HOST_CATALOG_PLUGIN,
TYPES_CREDENTIALS,
TYPE_CREDENTIAL_DYNAMIC,
} from 'api/models/host-catalog';
import { tracked } from '@glimmer/tracking';
//Note: this is a temporary solution till we have resource type helper in place
const icons = ['aws-color', 'azure-color'];
@@ -16,6 +19,21 @@ const icons = ['aws-color', 'azure-color'];
export default class FormHostCatalogAwsComponent extends Component {
// =properties
hostCatalogTypes = TYPES_HOST_CATALOG;
// =attributes
@tracked selectedCredentialType = this.args.model.credentialType;
@tracked showDynamicCredentials =
this.args.model.credentialType === TYPE_CREDENTIAL_DYNAMIC;
/**
* returns an array of available credential types
* @type {array}
*/
get credentials() {
return TYPES_CREDENTIALS;
}
/**
* maps resource type with icon
* @type {object}
@@ -32,4 +50,19 @@ export default class FormHostCatalogAwsComponent extends Component {
toggleDisableCredentialRotation(model) {
model.disable_credential_rotation = !model.disable_credential_rotation;
}
/**
* Allows to update the credential type
* @param type {string}
*/
@action
updateCredentialTypeSelection(type) {
this.selectedCredentialType = type;
if (type === TYPE_CREDENTIAL_DYNAMIC) {
this.showDynamicCredentials = true;
} else {
this.showDynamicCredentials = false;
}
this.args.changeCredentialType(type);
}
}
@@ -9,4 +9,5 @@
submit=@submit
cancel=@cancel
changeType=@changeType
changeCredentialType=@changeCredentialType
}}
@@ -7,6 +7,7 @@
class='full-width'
@cancel={{@cancel}}
@onSubmit={{this.submit}}
@disabled={{this.isSaving}}
as |form|
>
<div class='create-resources-form'>
@@ -9,12 +9,26 @@ import { tracked } from '@glimmer/tracking';
export default class FormOnboardingComponent extends Component {
// =properties
@tracked targetAddress;
@tracked targetAddressInvalid = false;
@tracked targetPort;
@tracked targetPortInvalid = false;
/**
* Returns true if any of the resources that are being created are still saving.
* @type {boolean}
*/
get isSaving() {
const { org, project, target, role } = this.args.model;
return org.isSaving || project.isSaving || target.isSaving || role.isSaving;
}
// =actions
/**
* Passes in target address and port to submit function for further proccessing.
*/
@action
submit() {
// Check targetAddress and targetPort are valid before submit
@@ -6,7 +6,7 @@
{{#if @model.totalItemsCount}}
<Rose::Form
class='full-width role-manage-custom-scopes-form'
@onSubmit={{fn @submit this.allGrantScopes}}
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.role.isSaving}}
as |form|
@@ -21,17 +21,17 @@
/>
</Hds::SegmentedGroup>
{{#if @model.orgScopes}}
{{#if this.selectedItems.length}}
<div class='table-selected-items-text'>
<div class='table-selected-items-text'>
{{#if @model.role.grantScopeOrgIDs.length}}
<Hds::Text::Body @tag='p' @color='faint'>
{{t
'resources.role.scope.titles.selected'
items=this.selectedItems.length
items=@model.role.grantScopeOrgIDs.length
total=@model.totalItemsCount
}}
</Hds::Text::Body>
</div>
{{/if}}
{{/if}}
</div>
<Hds::Table
@columns={{array
(hash label=(t 'resources.role.scope.title'))
@@ -46,7 +46,7 @@
<:body as |B|>
<B.Tr
@selectionKey={{B.data.id}}
@isSelected={{includes B.data.id this.selectedItems}}
@isSelected={{includes B.data.id @model.role.grantScopeOrgIDs}}
@selectionAriaLabelSuffix='row {{B.data.id}}'
data-test-org-scopes-table-row={{B.data.id}}
>
@@ -69,7 +69,7 @@
@iconPosition='trailing'
@text={{t
'resources.role.scope.actions.view-projects'
selected=projectTotals.selected
selected=projectTotals.selected.length
total=projectTotals.total
}}
@route='scopes.scope.roles.role.manage-scopes.manage-org-projects'
@@ -127,4 +127,63 @@
/>
</A.Footer>
</Hds::ApplicationState>
{{/if}}
{{#if this.selectedOrg}}
<Hds::Modal
@onClose={{this.toggleRemoveOrgModal}}
data-test-manage-scopes-remove-org-modal
as |M|
>
<M.Header>{{t 'resources.role.scope.remove-org.title'}}</M.Header>
<M.Body>
<Hds::Text::Body @tag='p' @color='primary'>
{{t
'resources.role.scope.remove-org.description'
orgDisplayName=this.orgDisplayName
}}
</Hds::Text::Body>
</M.Body>
<M.Footer as |F|>
<Hds::ButtonSet>
<Hds::Button
@text={{t 'resources.role.scope.actions.remove-org-and-projects'}}
{{on 'click' (fn this.removeProjects false)}}
/>
<Hds::Button
@color='secondary'
@text={{t 'resources.role.scope.actions.remove-org-only'}}
{{on 'click' F.close}}
/>
</Hds::ButtonSet>
</M.Footer>
</Hds::Modal>
{{/if}}
{{#if this.selectedOrgs}}
<Hds::Modal
@onClose={{this.toggleRemoveAllModal}}
data-test-manage-scopes-remove-all-orgs-modal
as |M|
>
<M.Header>{{t 'resources.role.scope.remove-all-orgs.title'}}</M.Header>
<M.Body>
<Hds::Text::Body @tag='p' @color='primary'>
{{t 'resources.role.scope.remove-all-orgs.description'}}
</Hds::Text::Body>
</M.Body>
<M.Footer as |F|>
<Hds::ButtonSet>
<Hds::Button
@text={{t 'resources.role.scope.actions.remove-orgs-and-projects'}}
{{on 'click' (fn this.removeProjects true)}}
/>
<Hds::Button
@color='secondary'
@text={{t 'resources.role.scope.actions.remove-orgs-only'}}
{{on 'click' F.close}}
/>
</Hds::ButtonSet>
</M.Footer>
</Hds::Modal>
{{/if}}
@@ -10,14 +10,18 @@ import { tracked } from '@glimmer/tracking';
export default class FormRoleManageCustomScopesIndexComponent extends Component {
// =attributes
@tracked selectedItems = [...this.args.model.role.grantScopeOrgIDs];
@tracked selectedOrgs = [];
@tracked selectedOrg = '';
get allGrantScopes() {
return [
...this.args.model.role.grantScopeKeywords,
...this.args.model.role.grantScopeProjectIDs,
...this.selectedItems,
];
/**
* Returns the display name of the selectedOrg.
* @type {string}
*/
get orgDisplayName() {
const org = this.args.model.orgScopes.find(
({ id }) => id === this.selectedOrg,
);
return org.displayName;
}
// =actions
@@ -25,19 +29,84 @@ export default class FormRoleManageCustomScopesIndexComponent extends Component
/**
* Handles the selection changes for the paginated table.
* @param {object} selectableRowsStates
* @param {object} selectionKey
*/
@action
selectionChange({ selectableRowsStates }) {
selectableRowsStates.forEach((row) => {
const { isSelected, selectionKey: key } = row;
const includesId = this.selectedItems.includes(key);
if (isSelected) {
if (!includesId) this.selectedItems = [...this.selectedItems, key];
} else if (includesId) {
this.selectedItems = this.selectedItems.filter(
(item) => item !== key,
);
selectionChange({ selectableRowsStates, selectionKey }) {
const { role, projectTotals } = this.args.model;
const addOrRemoveValues = (add, remove, orgId) => {
let selectedOrg;
const includesId = role.grant_scope_ids.includes(orgId);
const selected = projectTotals[orgId]?.selected;
if (add && !includesId) {
role.grant_scope_ids = [...role.grant_scope_ids, orgId];
} else if (remove && includesId) {
if (selected?.length) {
selectedOrg = orgId;
}
role.grant_scope_ids = role.grant_scope_ids.filter(
(item) => item !== orgId,
);
}
return selectedOrg;
};
if (selectionKey === 'all') {
const selectedOrgs = [];
selectableRowsStates.forEach(({ selectionKey, isSelected }) => {
const selectedOrg = addOrRemoveValues(
isSelected,
!isSelected,
selectionKey,
);
if (selectedOrg) {
selectedOrgs.push(selectedOrg);
}
});
this.selectedOrgs = selectedOrgs;
} else {
this.selectedOrg = addOrRemoveValues(true, true, selectionKey);
}
}
/**
* Removes projects from an org that was deselected and toggles the correct modal off.
* @param {boolean} toggleRemoveAllModal
*/
@action
removeProjects(toggleRemoveAllModal) {
const selectedOrgs = toggleRemoveAllModal
? this.selectedOrgs
: [this.selectedOrg];
const { role, projectTotals } = this.args.model;
selectedOrgs.forEach((orgId) => {
const { selected, total } = projectTotals[orgId];
role.grant_scope_ids = role.grant_scope_ids.filter(
(item) => !selected.includes(item),
);
projectTotals[orgId] = { selected: [], total };
});
if (toggleRemoveAllModal) {
this.toggleRemoveAllModal();
} else {
this.toggleRemoveOrgModal();
}
}
/**
* Toggles the modal to remove an org and projects off.
*/
@action
toggleRemoveOrgModal() {
this.selectedOrg = '';
}
/**
* Toggles the modal to remove orgs and projects off.
*/
@action
toggleRemoveAllModal() {
this.selectedOrgs = [];
}
}
@@ -6,7 +6,7 @@
{{#if @model.totalItemsCount}}
<Rose::Form
class='full-width role-manage-custom-scopes-form'
@onSubmit={{fn @submit this.allGrantScopes}}
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.role.isSaving}}
as |form|
@@ -21,17 +21,17 @@
/>
</Hds::SegmentedGroup>
{{#if @model.projectScopes}}
{{#if this.selectedItems.length}}
<div class='table-selected-items-text'>
<div class='table-selected-items-text'>
{{#if this.selectedProjectsCount}}
<Hds::Text::Body @tag='p' @color='faint'>
{{t
'resources.role.scope.titles.selected'
items=this.selectedItems.length
items=this.selectedProjectsCount
total=@model.totalItemsCount
}}
</Hds::Text::Body>
</div>
{{/if}}
{{/if}}
</div>
<Hds::Table
@columns={{array
(hash label=(t 'resources.project.title'))
@@ -45,7 +45,7 @@
<:body as |B|>
<B.Tr
@selectionKey={{B.data.id}}
@isSelected={{includes B.data.id this.selectedItems}}
@isSelected={{includes B.data.id @model.role.grant_scope_ids}}
@selectionAriaLabelSuffix='row {{B.data.id}}'
data-test-project-scopes-table-row={{B.data.id}}
>
@@ -5,20 +5,13 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class FormRoleManageOrgProjectsIndexComponent extends Component {
// =attributes
@tracked selectedItems = [...this.args.model.selectedProjectIDs];
get allGrantScopes() {
return [
...this.args.model.role.grantScopeKeywords,
...this.args.model.role.grantScopeOrgIDs,
...this.args.model.remainingProjectIDs,
...this.selectedItems,
];
get selectedProjectsCount() {
const { role, remainingProjectIDs } = this.args.model;
return role.grantScopeProjectIDs.length - remainingProjectIDs.length;
}
// =actions
@@ -29,16 +22,16 @@ export default class FormRoleManageOrgProjectsIndexComponent extends Component {
*/
@action
selectionChange({ selectableRowsStates }) {
const { role } = this.args.model;
selectableRowsStates.forEach((row) => {
const { isSelected, selectionKey: key } = row;
const includesId = this.selectedItems.includes(key);
if (isSelected) {
if (!includesId) this.selectedItems = [...this.selectedItems, key];
} else {
if (includesId)
this.selectedItems = this.selectedItems.filter(
(item) => item !== key,
);
const includesId = role.grant_scope_ids.includes(key);
if (isSelected && !includesId) {
role.grant_scope_ids = [...role.grant_scope_ids, key];
} else if (!isSelected && includesId) {
role.grant_scope_ids = role.grant_scope_ids.filter(
(item) => item !== key,
);
}
});
}
@@ -5,7 +5,7 @@
<Rose::Form
class='full-width role-manage-scopes-form'
@onSubmit={{fn @submit this.selectedItems}}
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
as |form|
@@ -14,7 +14,7 @@
<Hds::Form::Toggle::Field
@value={{this.keywords.keyThis}}
name={{this.keywords.keyThis}}
checked={{includes this.keywords.keyThis this.selectedItems}}
checked={{includes this.keywords.keyThis @model.grant_scope_ids}}
disabled={{form.disabled}}
{{on 'change' this.toggleField}}
as |F|
@@ -31,7 +31,7 @@
<Hds::Form::Toggle::Field
@value={{this.keywords.keyChildren}}
name={{this.keywords.keyChildren}}
checked={{includes this.keywords.keyChildren this.selectedItems}}
checked={{includes this.keywords.keyChildren @model.grant_scope_ids}}
disabled={{form.disabled}}
{{on 'change' this.toggleField}}
as |F|
@@ -48,7 +48,7 @@
<Hds::Form::Toggle::Field
@value={{this.keywords.keyDescendants}}
name={{this.keywords.keyDescendants}}
checked={{includes this.keywords.keyDescendants this.selectedItems}}
checked={{includes this.keywords.keyDescendants @model.grant_scope_ids}}
disabled={{form.disabled}}
{{on 'change' this.toggleField}}
as |F|
@@ -5,7 +5,6 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import {
GRANT_SCOPE_THIS,
GRANT_SCOPE_CHILDREN,
@@ -21,8 +20,6 @@ export default class FormRoleManageScopesIndexComponent extends Component {
keyDescendants: GRANT_SCOPE_DESCENDANTS,
};
@tracked selectedItems = [...this.args.model.grant_scope_ids];
/**
* Returns true if role is global level and either
* "children" or "descendants" is toggled on.
@@ -31,8 +28,8 @@ export default class FormRoleManageScopesIndexComponent extends Component {
get showAlert() {
return (
this.args.model.scope.isGlobal &&
(this.selectedItems.includes(GRANT_SCOPE_CHILDREN) ||
this.selectedItems.includes(GRANT_SCOPE_DESCENDANTS))
(this.args.model.grant_scope_ids.includes(GRANT_SCOPE_CHILDREN) ||
this.args.model.grant_scope_ids.includes(GRANT_SCOPE_DESCENDANTS))
);
}
@@ -46,10 +43,15 @@ export default class FormRoleManageScopesIndexComponent extends Component {
toggleField(event) {
const { checked, value } = event.target;
const removeValue = (value) => {
this.selectedItems = this.selectedItems.filter((item) => item !== value);
this.args.model.grant_scope_ids = this.args.model.grant_scope_ids.filter(
(item) => item !== value,
);
};
if (checked) {
this.selectedItems = [...this.selectedItems, value];
this.args.model.grant_scope_ids = [
...this.args.model.grant_scope_ids,
value,
];
if (value === GRANT_SCOPE_CHILDREN) {
removeValue(GRANT_SCOPE_DESCENDANTS);
}
@@ -424,7 +424,7 @@
{{t 'resources.storage-bucket.form.role_tags.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_tags.label'}}
{{t 'resources.storage-bucket.form.role_tags.help'}}
</F.HelperText>
{{#if @model.errors.role_tags}}
@@ -436,11 +436,18 @@
{{/if}}
</:fieldset>
<:field as |F|>
<F.TextInput
<F.KeyValue
@name='role_tags'
@options={{@model.role_tags}}
@model={{@model}}
/>
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
{{/if}}
@@ -3,35 +3,28 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
as |dropdown|
>
<Hds::Dropdown data-test-manage-host-catalogs-dropdown as |dd|>
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
{{#if (can 'create model' @model collection='hosts')}}
<dropdown.link
<dd.Interactive
@route='scopes.scope.host-catalogs.host-catalog.hosts.new'
@model={{@model}}
>
{{t 'resources.host.actions.create'}}
</dropdown.link>
@text={{t 'resources.host.actions.create'}}
/>
{{/if}}
{{#if (can 'create model' @model collection='host-sets')}}
<dropdown.link
<dd.Interactive
@route='scopes.scope.host-catalogs.host-catalog.host-sets.new'
@model={{@model}}
>
{{t 'resources.host-set.actions.create'}}
</dropdown.link>
@text={{t 'resources.host-set.actions.create'}}
/>
{{/if}}
{{#if (can 'delete model' @model)}}
<dropdown.separator />
<dropdown.button
@style='danger'
@disabled={{@model.canSave}}
<dd.Separator />
<dd.Interactive
@text={{t 'resources.host-catalog.actions.delete'}}
@color='critical'
{{on 'click' (fn @delete @model)}}
>
{{t 'resources.host-catalog.actions.delete'}}
</dropdown.button>
/>
{{/if}}
</Rose::Dropdown>
</Hds::Dropdown>
@@ -3,31 +3,25 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
as |dropdown|
>
<Hds::Dropdown data-test-manage-dropdown-host-sets as |dd|>
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
{{#if (can 'addHosts hostSet' @model)}}
<dropdown.link
<dd.Interactive
@text={{t 'resources.host-set.actions.create-and-add-host'}}
@route='scopes.scope.host-catalogs.host-catalog.host-sets.host-set.create-and-add-host'
>
{{t 'resources.host-set.actions.create-and-add-host'}}
</dropdown.link>
<dropdown.link
/>
<dd.Interactive
@text={{t 'resources.host-set.actions.add-hosts'}}
@route='scopes.scope.host-catalogs.host-catalog.host-sets.host-set.add-hosts'
>
{{t 'resources.host-set.actions.add-hosts'}}
</dropdown.link>
<dropdown.separator />
/>
<dd.Separator />
{{/if}}
{{#if (can 'delete model' @model)}}
<dropdown.button
@style='danger'
@disabled={{@model.canSave}}
<dd.Interactive
@text={{t 'resources.host-set.actions.delete'}}
@color='critical'
{{on 'click' (fn @delete @model)}}
>
{{t 'resources.host-set.actions.delete'}}
</dropdown.button>
/>
{{/if}}
</Rose::Dropdown>
</Hds::Dropdown>
@@ -4,17 +4,12 @@
}}
{{#if (can 'delete model' @model)}}
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
as |dropdown|
>
<dropdown.button
@style='danger'
@disabled={{@model.canSave}}
<Hds::Dropdown data-test-manage-hosts-dropdown as |dd|>
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
<dd.Interactive
@text={{t 'resources.host.actions.delete'}}
@color='critical'
{{on 'click' (fn @delete @model)}}
>
{{t 'resources.host.actions.delete'}}
</dropdown.button>
</Rose::Dropdown>
/>
</Hds::Dropdown>
{{/if}}
@@ -2,44 +2,39 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Dropdown data-test-manage-targets-dropdown as |dd|>
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
as |dropdown|
>
{{#if (can 'addHostSources target' @model)}}
<dropdown.link @route='scopes.scope.targets.target.add-host-sources'>
{{t 'resources.target.actions.add-host-sources'}}
</dropdown.link>
<dd.Interactive
@route='scopes.scope.targets.target.add-host-sources'
@text={{t 'resources.target.actions.add-host-sources'}}
/>
{{/if}}
{{#if (can 'addBrokeredCredentialSources target' @model)}}
<dropdown.link
<dd.Interactive
@route='scopes.scope.targets.target.add-brokered-credential-sources'
@text={{t 'resources.target.actions.add-brokered-credential-sources'}}
data-test-add-brokered-cred-sources-action
>
{{t 'resources.target.actions.add-brokered-credential-sources'}}
</dropdown.link>
/>
{{/if}}
{{#if (can 'addInjectedApplicationCredentialSources target' @model)}}
<dropdown.link
<dd.Interactive
@route='scopes.scope.targets.target.add-injected-application-credential-sources'
data-test-add-injected-cred-sources-action
>
{{t
@text={{t
'resources.target.actions.add-injected-application-credential-sources'
}}
</dropdown.link>
data-test-add-injected-cred-sources-action
/>
{{/if}}
{{#if (can 'delete model' @model)}}
<dropdown.separator />
<dropdown.button
@style='danger'
@disabled={{@model.canSave}}
<dd.Separator />
<dd.Interactive
@text={{t 'resources.target.actions.delete'}}
@color='critical'
{{on 'click' (fn @delete @model)}}
>
{{t 'resources.target.actions.delete'}}
</dropdown.button>
/>
{{/if}}
</Rose::Dropdown>
</Hds::Dropdown>
@@ -0,0 +1,126 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifyError } from 'core/decorators/notify';
export default class OnboardingIndexController extends Controller {
// =services
@service intl;
@service router;
// =actions
/**
* Creates resources then redirects to success page.
* @param {object} model
* @param {string} targetAddress
* @param {string} targetPort
*/
@action
@loading
@notifyError(function () {
return this.intl.t('errors.onboarding-failed.description');
})
async createResources(model, targetAddress, targetPort) {
const { org, project, target, role } = model;
await this.createOrg(org);
await this.createProject(org, project);
await this.createTarget(project, target, targetAddress, targetPort);
await this.createGrants(org, project, target, role);
// Redirect to success
this.router.transitionTo('onboarding.success');
}
/**
* Creates Org, if fails, redirects to Orgs empty list
* @param {ScopeModel} org
*/
async createOrg(org) {
try {
await org.save();
} catch (error) {
console.log('error ', error);
// Redirects to Orgs screen
this.router.transitionTo('scopes.scope', 'global');
throw new Error(error);
}
}
/**
* Creates Project, if fails redirects to Projects from Org empty list.
* @param {ScopeModel} org
* @param {ScopeModel} project
*/
async createProject(org, project) {
project.scopeID = org.id;
try {
await project.save();
} catch (error) {
// Redirects to Project screen
this.router.transitionTo('scopes.scope.scopes', project.scopeID);
throw new Error(error);
}
}
/**
* Creates a Target, if fails redirects to Targets empty list.
* @param {ScopeModel} project
* @param {TargetModel} target
* @param {string} targetAddress
* @param {string} targetPort
*/
async createTarget(project, target, targetAddress, targetPort) {
target.scopeID = project.id;
target.address = targetAddress;
target.default_port = targetPort;
try {
await target.save();
} catch (error) {
// Redirect to targets global
this.router.replaceWith('scopes.scope.targets', project.scopeID);
throw new Error(error);
}
}
/**
* Assigns default grants and scope to a role.
* @param {ScopeModel} org
* @param {ScopeModel} project
* @param {TargetModel} target
* @param {RoleModel} role
*/
async createGrants(org, project, target, role) {
role.scopeID = org.id;
role.grant_scope_id = project.id;
try {
await role.save();
await role.saveGrantStrings([
`type=target;actions=list`,
`ids=${target.id};actions=authorize-session`,
]);
} catch (error) {
// Redirect to targets that belong to scope
this.router.replaceWith('scopes.scope.targets', project.scopeID);
throw new Error(error);
}
}
/**
* Redirects user to index route for further processing.
*/
@action
doLater() {
this.router.transitionTo('index');
}
}
@@ -0,0 +1,29 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class OnboardingSuccessController extends Controller {
// =services
@service router;
// =actions
/**
* Redirect user to target created during onboarding.
* @param {object} model
*/
@action
showTargetList(model) {
const {
project: { id: scopeID },
target: { id: targetID },
} = model;
this.router.transitionTo('scopes.scope.targets.target', scopeID, targetID);
}
}
@@ -6,15 +6,26 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { loading } from 'ember-loading';
import { debounce } from 'core/decorators/debounce';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { confirm } from 'core/decorators/confirm';
export default class ScopesScopeAliasesIndexController extends Controller {
// =services
@service can;
@service router;
// =attributes
queryParams = ['search', 'page', 'pageSize'];
@tracked search;
@tracked page = 1;
@tracked pageSize = 10;
// =actions
/**
@@ -22,10 +33,10 @@ export default class ScopesScopeAliasesIndexController extends Controller {
* @param {AliasModel} alias
*/
@action
async cancel(alias) {
cancel(alias) {
const { isNew } = alias;
alias.rollbackAttributes();
if (isNew) await this.router.transitionTo('scopes.scope.aliases');
if (isNew) this.router.transitionTo('scopes.scope.aliases');
}
/**
@@ -49,7 +60,7 @@ export default class ScopesScopeAliasesIndexController extends Controller {
}
/**
* Remove destaination_id from alias
* Remove destination_id from alias
* @param {AliasModel} alias
*/
@action
@@ -77,4 +88,16 @@ export default class ScopesScopeAliasesIndexController extends Controller {
await this.router.replaceWith('scopes.scope.aliases');
await this.router.refresh();
}
/**
* Handles input on each keystroke and the search queryParam
* @param {object} event
*/
@action
@debounce(250)
handleSearchInput(event) {
const { value } = event.target;
this.search = value;
this.page = 1;
}
}
@@ -0,0 +1,74 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { getOwner } from '@ember/application';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifyError } from 'core/decorators/notify';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
export default class ScopesScopeAuthenticateMethodIndexController extends Controller {
// =services
@service session;
@service windowManager;
@service router;
// =actions
/**
* Delegates authentication to an authenticator of type matching the current
* auth method. If authentication fails, notifies with an alert.
*/
@action
@loading
@notifyError(() => 'errors.authentication-failed.title', {
sticky: false,
catch: true,
})
async authenticate(authMethod, creds) {
const scope = authMethod.scope;
const authenticatorName = `authenticator:${authMethod.type}`;
const requestCookies = false;
switch (authMethod.type) {
case TYPE_AUTH_METHOD_PASSWORD:
case TYPE_AUTH_METHOD_LDAP:
await this.session.authenticate(
authenticatorName,
creds,
requestCookies,
{ scope, authMethod },
);
this.router.transitionTo('index');
break;
case TYPE_AUTH_METHOD_OIDC:
await this.startOIDCAuthentication(authenticatorName, {
scope,
authMethod,
});
this.router.transitionTo('scopes.scope.authenticate.method.oidc');
break;
}
}
/**
* Kicks off the OIDC auth flow by opening a new window to the provider.
* @param {string} authenticatorName
* @param {object} options
*/
async startOIDCAuthentication(authenticatorName, options) {
const oidc = getOwner(this).lookup(authenticatorName);
// TODO: delegate this call from the session service so that we don't have
// to look up the authenticator directly
const json = await oidc.startAuthentication(options);
await this.windowManager.open(json.attributes.auth_url);
}
}
@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeEditController extends Controller {
@controller('scopes/scope/index') scopes;
}
@@ -99,4 +99,14 @@ export default class ScopesScopeHostCatalogsIndexController extends Controller {
if (type === 'plugin') type = 'aws';
this.router.replaceWith({ queryParams: { type } });
}
/**
* Updates credential type
* @param {model} hostcatalog
* @param {string} credentialType
*/
@action
changeCredentialType(hostCatalog, credentialType) {
hostCatalog.credentialType = credentialType;
}
}
@@ -0,0 +1,77 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeIndexController extends Controller {
// =services
@service router;
//= actions
/**
* Rollback changes on scope.
* @param {ScopeModel} scope
*/
@action
cancel(scope) {
const { isNew } = scope;
scope.rollbackAttributes();
if (isNew) this.router.transitionTo('scopes.scope');
}
/**
* Handle save scope.
* @param {ScopeModel} scope
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(scope) {
const { isNew } = scope;
await scope.save();
await this.router.transitionTo('scopes.scope.edit', scope);
if (isNew) this.router.refresh();
}
/**
* Deletes the scope and redirects to index.
* @param {ScopeModel} scope
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(scope) {
const { scopeID } = scope;
await scope.destroyRecord();
this.router.replaceWith('scopes.scope.scopes', scopeID);
}
/**
* Deletes the scope and redirects to index.
* @param {ScopeModel} scope
*/
@action
@loading
@confirm('resources.policy.questions.detach')
@notifyError(({ message }) => message)
@notifySuccess('resources.policy.messages.detach')
async detachStoragePolicy(scope) {
const { storage_policy_id } = scope;
scope.storage_policy_id = '';
await scope.detachStoragePolicy(storage_policy_id);
}
}
@@ -23,22 +23,23 @@ export default class ScopesScopeRolesRoleManageScopesIndexController extends Con
/**
* Save grant scope IDs to current role via the API.
* @param {RoleModel} role
* @param {[string]} grantScopeIDs
*/
@action
@loading
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('resources.role.scope.messages.manage-scopes.success')
async setGrantScopes(role, grantScopeIDs) {
await role.setGrantScopes(grantScopeIDs);
async setGrantScopes(role) {
await role.setGrantScopes(role.grant_scope_ids);
this.router.replaceWith('scopes.scope.roles.role.scopes');
}
/**
* Redirect to role scopes as if nothing ever happened.
* @param {RoleModel} role
*/
@action
cancel() {
cancel(role) {
role.rollbackAttributes();
this.router.replaceWith('scopes.scope.roles.role.scopes');
}
}
@@ -43,23 +43,24 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesControlle
/**
* Save grant scope IDs to current role via the API.
* @param {RoleModel} role
* @param {[string]} grantScopeIDs
*/
@action
@loading
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('resources.role.scope.messages.manage-custom-scopes.success')
async setGrantScopes(role, grantScopeIDs) {
await role.setGrantScopes(grantScopeIDs);
async setGrantScopes(role) {
await role.setGrantScopes(role.grant_scope_ids);
this.manageScopes.showCheckIcon = true;
await this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
}
/**
* Redirect to manage scopes as if nothing ever happened.
* @param {RoleModel} role
*/
@action
async cancel() {
await this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
cancel(role) {
role.rollbackAttributes();
this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
}
}
@@ -43,14 +43,13 @@ export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsController
/**
* Save grant scope IDs to current role via the API.
* @param {RoleModel} role
* @param {[string]} grantScopeIDs
*/
@action
@loading
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('resources.role.scope.messages.manage-org-projects.success')
async setGrantScopes(role, grantScopeIDs) {
await role.setGrantScopes(grantScopeIDs);
async setGrantScopes(role) {
await role.setGrantScopes(role.grant_scope_ids);
this.manageScopes.showCheckIcon = true;
if (role.scope.isGlobal) {
this.router.replaceWith(
@@ -63,9 +62,11 @@ export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsController
/**
* Redirect to previous as if nothing ever happened.
* @param {RoleModel} role
*/
@action
cancel(role) {
role.rollbackAttributes();
if (role.scope.isGlobal) {
this.router.replaceWith(
'scopes.scope.roles.role.manage-scopes.manage-custom-scopes',
@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeScopesNewController extends Controller {
@controller('scopes/scope/index') scopes;
}
@@ -4,21 +4,192 @@
*/
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { debounce } from 'core/decorators/debounce';
import { inject as service } from '@ember/service';
export default class ScopesScopeSessionRecordingsIndexController extends Controller {
// =services
@service store;
@service intl;
// =attributes
queryParams = [
'search',
'time',
{ users: { type: 'array' } },
{ scopes: { type: 'array' } },
{ targets: { type: 'array' } },
'page',
'pageSize',
];
now = new Date();
@tracked search = '';
@tracked time = null;
@tracked users = [];
@tracked scopes = [];
@tracked targets = [];
@tracked page = 1;
@tracked pageSize = 10;
/**
* Returns true if any session recordings exist
* @type {boolean}
* Returns object of filters to be used for displaying selected filters
* @returns {object}
*/
get hasSessionRecordings() {
return this.model?.sessionRecordings?.length;
get filters() {
return {
allFilters: {
time: this.timeOptions,
users: this.filterOptions('user'),
scopes: this.projectScopes,
targets: this.filterOptions('target'),
},
selectedFilters: {
time: [this.time],
users: this.users,
scopes: this.scopes,
targets: this.targets,
},
};
}
/**
* Returns true if any storage buckets exist
* @type {boolean}
* Returns array of time options for time filter
* @returns {[object]}
*/
get hasSessionRecordingsConfigured() {
return this.model?.storageBuckets?.length;
get timeOptions() {
const last24Hours = new Date(this.now.getTime() - 24 * 60 * 60 * 1000);
const last3Days = new Date(this.now);
last3Days.setDate(this.now.getDate() - 3);
const last7Days = new Date(this.now);
last7Days.setDate(this.now.getDate() - 7);
return [
{
id: last24Hours.toISOString(),
name: this.intl.t(
'resources.session-recording.filters.time.last-twenty-four-hours',
),
},
{
id: last3Days.toISOString(),
name: this.intl.t(
'resources.session-recording.filters.time.last-three-days',
),
},
{
id: last7Days.toISOString(),
name: this.intl.t(
'resources.session-recording.filters.time.last-seven-days',
),
},
];
}
/**
* Returns unique project scopes from targets
* linked to the session recordings
* @returns {[object]}
*/
get projectScopes() {
const uniqueMap = new Map();
this.model.allSessionRecordings.forEach(
({
create_time_values: {
target: {
scope: { id, name, parent_scope_id },
},
},
}) => {
if (!uniqueMap.has(id)) {
const projectName = name || id;
uniqueMap.set(id, { id, name: projectName, parent_scope_id });
}
},
);
return Array.from(uniqueMap.values());
}
// =actions
/**
* Looks up org by ID and returns the org name
* @param {string} orgID
* @returns {string}
*/
@action
orgName(orgID) {
const org = this.store.peekRecord('scope', orgID);
return org.displayName;
}
/**
* Returns all filter options for key for session recordings
* @param {string} key
* @returns {[object]}
*/
@action
filterOptions(key) {
const uniqueMap = new Map();
this.model.allSessionRecordings.forEach(
({
create_time_values: {
[key]: { id, name },
},
}) => {
if (!uniqueMap.has(id)) {
uniqueMap.set(id, { id, name });
}
},
);
return Array.from(uniqueMap.values());
}
/**
* Handles input on each keystroke and the search queryParam
* @param {object} event
*/
@action
@debounce(250)
handleSearchInput(event) {
const { value } = event.target;
this.search = value;
this.page = 1;
}
/**
* Sets the selected items for the given paramKey and sets the page to 1
* @param {string} paramKey
* @param {[string]} selectedItems
*/
@action
applyFilter(paramKey, selectedItems) {
this[paramKey] = [...selectedItems];
this.page = 1;
}
/**
* Sets the time filter, sets the page to 1, and closes the filter dropdown
* @param {object} timeId
* @param {func} onClose
*/
@action
changeTimeFilter(timeId, onClose) {
this.time = timeId;
this.page = 1;
onClose();
}
/**
* Refreshes the all data for the current page
*/
@action
refresh() {
this.send('refreshAll');
}
}
@@ -4,15 +4,26 @@
*/
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import {
STATE_SESSION_RECORDING_STARTED,
STATE_SESSION_RECORDING_UNKNOWN,
} from 'api/models/session-recording';
export default class ScopesScopeSessionRecordingsSessionRecordingChannelsByConnectionIndexController extends Controller {
// =services
@service router;
// =attributes
/**
* Returns true if the session recording has started
* but has not started any connections
* but has not started any connections.
* @type {boolean}
*/
get isSessionInprogressWithNoConnections() {
@@ -24,7 +35,7 @@ export default class ScopesScopeSessionRecordingsSessionRecordingChannelsByConne
/**
* Returns true if the session recording state is unknown
* and has no connections
* and has no connections.
* @type {boolean}
*/
get isSessionUnknownWithNoConnections() {
@@ -33,4 +44,42 @@ export default class ScopesScopeSessionRecordingsSessionRecordingChannelsByConne
this.model?.sessionRecording?.state === STATE_SESSION_RECORDING_UNKNOWN
);
}
// =actions
/**
* Reapplies storage policy dates to session recording.
* @param {SessionRecordingModel}
*/
@action
@notifyError(({ message }) => message, { catch: true })
@notifySuccess(({ end_time }) =>
!end_time
? 'resources.policy.messages.later'
: 'resources.policy.messages.reapply',
)
async reapplyStoragePolicy(sessionRecording) {
try {
await sessionRecording.reapplyStoragePolicy();
} catch (e) {
this.router.replaceWith('scopes.scope.session-recordings');
this.router.refresh();
throw new Error(e);
}
}
/**
* Deletes the session recording.
* @param {SessionRecording} recording
*/
@action
@loading
@confirm('resources.session-recording.questions.delete')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(sessionRecording) {
await sessionRecording.destroyRecord();
this.router.replaceWith('scopes.scope.session-recordings');
this.router.refresh();
}
}
@@ -3,8 +3,6 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import fetch from 'fetch';
async function autoInitializeFeatureEdition(owner) {
const service = owner.lookup('service:feature-edition');
const adapter = owner.lookup('adapter:application');
+2 -1
View File
@@ -43,7 +43,8 @@ export default class ApplicationRoute extends Route {
this.router.on('routeWillChange', async (transition) => {
const fromName = transition?.from?.name;
const toName = transition?.to?.name;
const maybeModel = transition?.from?.attributes;
const maybeModel =
transition?.data?.model ?? transition?.from?.attributes;
if (fromName !== toName && maybeModel?.hasDirtyAttributes) {
transition.abort();
try {
+1 -96
View File
@@ -5,9 +5,6 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifyError } from 'core/decorators/notify';
import { TYPE_TARGET_TCP } from 'api/models/target';
export default class OnboardingRoute extends Route {
@@ -16,7 +13,6 @@ export default class OnboardingRoute extends Route {
@service store;
@service session;
@service router;
@service intl;
// =methods
@@ -34,7 +30,7 @@ export default class OnboardingRoute extends Route {
/**
* List all scopes (orgs) under global.
* Creare org, project, target and roles
* Create org, project, target and roles
*/
async model() {
return {
@@ -59,95 +55,4 @@ export default class OnboardingRoute extends Route {
}),
};
}
// =actions
@action
@loading
@notifyError(function () {
return this.intl.t('errors.onboarding-failed.description');
})
async createResources(targetAddress, targetPort) {
const { org, project, target, role } = this.currentModel;
await this.createOrg(org);
await this.createProject(org, project);
await this.createTarget(project, target, targetAddress, targetPort);
await this.createGrants(org, project, target, role);
// Redirect to success
this.router.transitionTo('onboarding.success');
}
/**
* Creates Org, if fails, redirects to Orgs empty list
* @param {Object} org
*/
async createOrg(org) {
try {
await org.save();
} catch (error) {
// Redirects to Orgs screen
this.router.transitionTo('scopes.scope', 'global');
throw new Error(error);
}
}
/**
* Creates Project, if fails redirects to Projects from Org empty list.
* @param {Object} org
* @param {Object} project
*/
async createProject(org, project) {
project.scopeID = org.id;
try {
await project.save();
} catch (error) {
// Redirects to Project screen
this.router.transitionTo('scopes.scope.scopes', project.scopeID);
throw new Error(error);
}
}
/**
* Creates a Target, if fails redirects to Targets empyt list.
* @param {Object} project
* @param {Object} target
* @param {Object} targetAddress
* @param {Object} targetPort
*/
async createTarget(project, target, targetAddress, targetPort) {
target.scopeID = project.id;
target.address = targetAddress;
target.default_port = targetPort;
try {
await target.save();
} catch (error) {
// Redirect to targets global
this.router.replaceWith('scopes.scope.targets', project.scopeID);
throw new Error(error);
}
}
async createGrants(org, project, target, role) {
role.scopeID = org.id;
role.grant_scope_ids = [project.id];
try {
await role.save();
await role.saveGrantStrings([
`type=target;actions=list`,
`ids=${target.id};actions=authorize-session`,
]);
} catch (error) {
// Redirect to targets that belong to scope
this.router.replaceWith('scopes.scope.targets', project.scopeID);
throw new Error(error);
}
}
@action
async doLater() {
this.router.transitionTo('index');
}
}
+2 -12
View File
@@ -5,15 +5,14 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class OnboardingSuccessRoute extends Route {
// =services
@service router;
@service session;
@service('browser/window') window;
// =methods
model() {
const model = this.modelFor('onboarding');
return {
@@ -21,13 +20,4 @@ export default class OnboardingSuccessRoute extends Route {
clusterUrl: this.window.origin,
};
}
@action
showTargetList(scope) {
const {
project: { id: scopeID },
target: { id: targetID },
} = scope;
this.router.transitionTo('scopes.scope.targets.target', scopeID, targetID);
}
}
-19
View File
@@ -6,10 +6,6 @@
import Route from '@ember/routing/route';
import { A } from '@ember/array';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesRoute extends Route {
// =services
@@ -41,19 +37,4 @@ export default class ScopesRoute extends Route {
})
.catch(() => A([]));
}
/**
* Deletes the scope and redirects to index.
* @param {Model} scope
*/
@action
@loading
@confirm('resources.policy.questions.detach')
@notifyError(({ message }) => message)
@notifySuccess('resources.policy.messages.detach')
async detachStoragePolicy(scope) {
const { storage_policy_id } = scope;
scope.storage_policy_id = '';
await scope.detachStoragePolicy(storage_policy_id);
}
}
-56
View File
@@ -6,21 +6,13 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { A } from '@ember/array';
import { action } from '@ember/object';
import { next } from '@ember/runloop';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { TYPE_SCOPE_GLOBAL, TYPE_SCOPE_ORG } from 'api/models/scope';
export default class ScopesScopeRoute extends Route {
// =services
@service store;
@service intl;
@service session;
@service scope;
@service router;
// =methods
@@ -105,52 +97,4 @@ export default class ScopesScopeRoute extends Route {
/* eslint-disable-next-line ember/no-controller-access-in-routes */
this.controller.setProperties({ scopes });
}
// =actions
/**
* Rollback changes on scope.
* @param {Model} scope
*/
@action
cancel(scope) {
const { isNew } = scope;
next(() => {
scope.rollbackAttributes();
if (isNew) this.router.transitionTo('scopes.scope');
});
}
/**
* Handle save scope.
* @param {Model} scope
* @param {Event} e
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(scope) {
const { isNew } = scope;
await scope.save();
await this.router.transitionTo('scopes.scope.edit', scope);
if (isNew) this.refresh();
}
/**
* Deletes the scope and redirects to index.
* @param {Model} scope
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(scope) {
const { scopeID } = scope;
await scope.destroyRecord();
this.router.replaceWith('scopes.scope.scopes', scopeID);
}
}
@@ -5,14 +5,11 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { all, hash } from 'rsvp';
export default class ScopesScopeAliasesRoute extends Route {
// =services
@service store;
@service session;
@service can;
@service router;
// =methods
@@ -23,34 +20,4 @@ export default class ScopesScopeAliasesRoute extends Route {
beforeModel() {
if (!this.session.isAuthenticated) this.router.transitionTo('index');
}
/**
* Load all aliases in current scope
* @return {Promise<[AliasModel]>}
*/
async model() {
const scope = this.modelFor('scopes.scope');
const { id: scope_id } = scope;
if (
this.can.can('list model', scope, {
collection: 'aliases',
})
) {
const aliases = await this.store.query('alias', { scope_id });
// since we don't receive target info from aliases list API,
// we query the store to fetch target information based on the destination id
const aliasesWithResourceNames = await all(
aliases.map((alias) =>
hash({
alias,
target: alias.destination_id
? this.store.findRecord('target', alias.destination_id)
: null,
}),
),
);
return aliasesWithResourceNames;
}
}
}
@@ -4,10 +4,100 @@
*/
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
export default class ScopesScopeAliasesIndexRoute extends Route {
// =services
@service store;
@service can;
// =attributes
queryParams = {
search: {
refreshModel: true,
replace: true,
},
page: {
refreshModel: true,
},
pageSize: {
refreshModel: true,
},
};
// =methods
/**
* Loads queried aliases and the number of aliases under current scope.
* @returns {Promise<{totalItems: number, aliases: [object], aliasesExist: boolean}>}
*/
async model({ search, page, pageSize }) {
const scope = this.modelFor('scopes.scope');
const { id: scope_id } = scope;
let aliases;
let doAliasesExist = false;
let totalItems = 0;
if (
this.can.can('list model', scope, {
collection: 'aliases',
})
) {
aliases = await this.store.query('alias', {
scope_id,
query: { search },
page,
pageSize,
});
totalItems = aliases.meta?.totalItems;
// since we don't receive target info from aliases list API,
// we query the store to fetch target information based on the destination id
aliases = await Promise.all(
aliases.map((alias) =>
hash({
alias,
target: alias.destination_id
? this.store.findRecord('target', alias.destination_id, {
backgroundReload: false,
})
: null,
}),
),
);
doAliasesExist = await this.getDoAliasesExist(scope_id, totalItems);
}
return { aliases, doAliasesExist, totalItems };
}
/**
* Sets doAliasesExist to true if there are any aliases.
* @param {string} scope_id
* @param {number} totalItems
* @returns {Promise<boolean>}
*/
async getDoAliasesExist(scope_id, totalItems) {
if (totalItems > 0) {
return true;
}
const options = { pushToStore: false, peekIndexedDB: true };
const aliases = await this.store.query(
'alias',
{
scope_id,
query: { filters: { scope_id: [{ equals: scope_id }] } },
page: 1,
pageSize: 1,
},
options,
);
return aliases.length > 0;
}
setupController(controller) {
const scope = this.modelFor('scopes.scope');
super.setupController(...arguments);
@@ -40,7 +40,7 @@ export default class ScopesScopeAuthMethodsIndexRoute extends Route {
/**
* Loads queried auth-methods and the number of auth-methods under current scope.
* @returns {Promise<{totalItems: number, authMethods: [AuthMethodModel], authMethodsExist: boolean }> }
* @returns {Promise<{totalItems: number, authMethods: [AuthMethodModel], doAuthMethodsExist: boolean }> }
*/
async model({ search, types, primary, page, pageSize }) {
const scope = this.modelFor('scopes.scope');
@@ -62,7 +62,7 @@ export default class ScopesScopeAuthMethodsIndexRoute extends Route {
let authMethods;
let totalItems = 0;
let authMethodsExist = false;
let doAuthMethodsExist = false;
if (this.can.can('list model', scope, { collection: 'auth-methods' })) {
// TODO: Remove storeToken option as this is a temporary fix for auth-methods.
const options = { storeToken: false };
@@ -77,19 +77,22 @@ export default class ScopesScopeAuthMethodsIndexRoute extends Route {
options,
);
totalItems = authMethods.meta?.totalItems;
authMethodsExist = await this.getAuthMethodsExist(scope_id, totalItems);
doAuthMethodsExist = await this.getDoAuthMethodsExist(
scope_id,
totalItems,
);
}
return { authMethods, authMethodsExist, totalItems };
return { authMethods, doAuthMethodsExist, totalItems };
}
/**
* Sets authMethodsExist to true if there exists any auth methods.
* Sets doAuthMethodsExist to true if there exists any auth methods.
* @param {string} scope_id
* @param {number} totalItems
* @returns {Promise<boolean>}
*/
async getAuthMethodsExist(scope_id, totalItems) {
async getDoAuthMethodsExist(scope_id, totalItems) {
if (totalItems > 0) {
return true;
}
@@ -5,24 +5,11 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { getOwner } from '@ember/application';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifyError } from 'core/decorators/notify';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
export default class ScopesScopeAuthenticateMethodRoute extends Route {
// =services
@service store;
@service session;
@service intl;
@service windowManager;
@service router;
// =methods
@@ -35,55 +22,4 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
const adapterOptions = { scopeID: this.modelFor('scopes.scope').id };
return this.store.peekRecord('auth-method', id, adapterOptions);
}
/**
* Kicks off the OIDC auth flow by opening a new window to the provider.
* @param {string} authenticatorName
* @param {object} options
*/
async startOIDCAuthentication(authenticatorName, options) {
const oidc = getOwner(this).lookup(authenticatorName);
// TODO: delegate this call from the session service so that we don't have
// to look up the authenticator directly
const json = await oidc.startAuthentication(options);
await this.windowManager.open(json.attributes.auth_url);
}
// =actions
/**
* Delegates authentication to an authenticator of type matching the current
* auth method. If authentication fails, notifies with an alert.
*/
@action
@loading
@notifyError(() => 'errors.authentication-failed.title', {
sticky: false,
catch: true,
})
async authenticate(creds) {
const scope = this.modelFor('scopes.scope');
const authMethod = this.modelFor('scopes.scope.authenticate.method');
const authenticatorName = `authenticator:${authMethod.type}`;
const requestCookies = false;
switch (authMethod.type) {
case TYPE_AUTH_METHOD_PASSWORD:
case TYPE_AUTH_METHOD_LDAP:
await this.session.authenticate(
authenticatorName,
creds,
requestCookies,
{ scope, authMethod },
);
this.router.transitionTo('index');
break;
case TYPE_AUTH_METHOD_OIDC:
await this.startOIDCAuthentication(authenticatorName, {
scope,
authMethod,
});
this.router.transitionTo('scopes.scope.authenticate.method.oidc');
break;
}
}
}
@@ -35,7 +35,7 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route {
/**
* Loads queried credential-stores and the number of credential-stores under current scope.
* @returns {Promise<{totalItems: number, credentialStores: [CredentialStoreModel], credentialStoresExist: boolean }> }
* @returns {Promise<{totalItems: number, credentialStores: [CredentialStoreModel], doCredentialStoresExist: boolean }> }
*/
async model({ search, types, page, pageSize }) {
const scope = this.modelFor('scopes.scope');
@@ -51,7 +51,7 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route {
let credentialStores;
let totalItems = 0;
let credentialStoresExist = false;
let doCredentialStoresExist = false;
if (
this.can.can('list model', scope, {
collection: 'credential-stores',
@@ -64,22 +64,21 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route {
pageSize,
});
totalItems = credentialStores.meta?.totalItems;
credentialStoresExist = await this.getCredentialStoresExist(
doCredentialStoresExist = await this.getDoCredentialStoresExist(
scope_id,
totalItems,
);
}
return { credentialStores, credentialStoresExist, totalItems };
return { credentialStores, doCredentialStoresExist, totalItems };
}
/**
* Sets credentialStoresExist to true if there exists any credential-stores.
* Sets doCredentialStoresExist to true if there exists any credential-stores.
* @param {string} scope_id
* @param {number} totalItems
* @returns {Promise<boolean>}
*/
async getCredentialStoresExist(scope_id, totalItems) {
async getDoCredentialStoresExist(scope_id, totalItems) {
if (totalItems > 0) {
return true;
}
Loaded 100 of 457 files, more files were not shown because too many files have changed in this diff. Show more