Compare commits

...
18 Commits
Author SHA1 Message Date
798cfc6828 chore: 🤖 Update desktop client changelog (#2183)
Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
2024-03-19 12:23:50 -07:00
Lisbet Alvarez d93986c4e2 refactor: 💡 remove route-action usage from credential-stores (#2172)
* refactor: 💡 remove route-action usage from credential-stores

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12604
2024-03-18 16:34:37 -07:00
Michael Li 879c196887 refact(e2e): Move env outside of helper methods (#2186)
* refact(e2e): Move env outside of UI helper methods

* refact(e2e): Move env outside of CLI helpers
2024-03-18 15:53:39 -04:00
Lisbet Alvarez 98170ed01e refactor: 💡 Remove route-action usage for managed-groups (#2168)
* refactor: 💡 Remove route-action usage for managed-groups

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12619
2024-03-18 10:34:54 -07:00
Dharini Jeeva 47a218cf8f chore: Add routes for aliases (#2185)
alias resource routes
2024-03-15 14:48:32 -07:00
Dharini Jeeva 879a58dcb8 feat: 🎸 mirage support and initial route for aliases (#2181)
* feat: 🎸 mirage support and initial route for aliases
2024-03-15 14:24:20 -07:00
Stan RyzhovandMichael Li f100190ac1 test: 💍 Add storage policies e2e-test (#2175)
* test: 💍 Add storage policies e2e-test

✅ Closes: ICU-12883

* chore: Adjust required env for e2e-tests

✅ Closes: ICU-12883

* CR: Refactor helper method to pass in env

---------

Co-authored-by: Michael Li <michael.li@hashicorp.com>
2024-03-15 11:55:13 -04:00
Lisbet Alvarez 0902e42cb0 refactor: 💡 remove route-action usage for groups (#2176)
* refactor: 💡 remove route-action usage for groups

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12605
2024-03-14 13:28:43 -07:00
Michael Li 6b0958e4d8 test(e2e): Admin UI test fixes (#2182) 2024-03-14 14:17:54 -04:00
Cameron Perera 82ea70f44b fix: 🐛 update destination path for autoUpdater workflow (#2179)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12878
2024-03-13 10:46:22 -05:00
hashicorp-copywrite[bot] 0bb72040db [COMPLIANCE] Add Copyright and License Headers (#2177)
Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2024-03-11 11:51:45 -05:00
Michael Li 9d9a38510b test(e2e): Add test for SSH certification injection (#2170) 2024-03-07 17:05:45 -05:00
Lisbet Alvarez a54aecee07 refactor: 💡 remove route-action usage in accounts (#2134)
* refactor: 💡 remove route-action usage in accounts

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12616
2024-03-07 09:24:28 -08:00
Devon Anderson 7dab184bb9 chore: 🤖 Migrate form.input to Hds:TExtInput::Field (#2149)
* chore: 🤖 Migrate `form.input` to `Hds:TExtInput::Field`

Migrate `form/account/oidc` and `form/account/password` forms to use
`Hds::TextInput::Field` instead of `form.input`

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

* refactor: 💡 argument clean up

argument clean up

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

* refactor: 💡 switch to `readonly` arument instead of `disabled`

switch to `readonly` arument instead of `disabled`

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

* refactor: 💡 fix incorrect attributes

fix incorrect attributes

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

* refactor: 💡 fix failing test

fix failing test

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12457
2024-03-07 10:12:33 -05:00
Lisbet Alvarez f068ac0b0c refactor: 💡 remove usage of route-action for app route (#2135)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12615
2024-03-04 10:13:59 -08:00
Michael Li d07c33ca41 test(e2e): Add test for SSH Credential Injection with Vault SSH Private Key (#2166) 2024-03-01 13:35:47 -05:00
hashicorp-tsccr[bot]andhashicorp-tsccr[bot] f073de774d Result of tsccr-helper -log-level=info gha update -latest . (#2167)
Co-authored-by: hashicorp-tsccr[bot] <hashicorp-tsccr[bot]@users.noreply.github.com>
2024-03-01 09:48:29 -08:00
Michael Li 1040a05880 test(e2e): Add LDAP Admin UI e2e test (#2164) 2024-02-29 16:51:32 -05:00
142 changed files with 3120 additions and 986 deletions

No files matched your search

+8 -8
View File
@@ -19,7 +19,7 @@ jobs:
key: node-modules-${{ hashFiles('**/yarn.lock') }} key: node-modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: | restore-keys: |
node-modules- node-modules-
- uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
node-version: 16 node-version: 16
- run: yarn install - run: yarn install
@@ -37,17 +37,17 @@ jobs:
restore-keys: | restore-keys: |
node-modules- node-modules-
- name: Build - name: Build
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
node-version: 16 node-version: 16
- run: yarn run build:ui:admin:oss - run: yarn run build:ui:admin:oss
- name: Upload artifact admin-ui - name: Upload artifact admin-ui
uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with: with:
name: admin-ui name: admin-ui
path: ./ui/admin/dist/ path: ./ui/admin/dist/
- name: Upload artifact OSS admin UI - name: Upload artifact OSS admin UI
uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with: with:
name: admin-ui-oss name: admin-ui-oss
path: ./ui/admin/dist/ path: ./ui/admin/dist/
@@ -65,12 +65,12 @@ jobs:
restore-keys: | restore-keys: |
node-modules- node-modules-
- name: Build - name: Build
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
node-version: 16 node-version: 16
- run: yarn run build:ui:admin:enterprise - run: yarn run build:ui:admin:enterprise
- name: Upload artifact enterprise admin UI - name: Upload artifact enterprise admin UI
uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with: with:
name: admin-ui-ent name: admin-ui-ent
path: ./ui/admin/dist/ path: ./ui/admin/dist/
@@ -88,12 +88,12 @@ jobs:
restore-keys: | restore-keys: |
node-modules- node-modules-
- name: Build - name: Build
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
node-version: 16 node-version: 16
- run: yarn run build:ui:admin:hcp - run: yarn run build:ui:admin:hcp
- name: Upload artifact HCP admin UI - name: Upload artifact HCP admin UI
uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with: with:
name: admin-ui-hcp name: admin-ui-hcp
path: ./ui/admin/dist/ path: ./ui/admin/dist/
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@e6a47e25652c5dbefe5b65d3b8e8dab0e9364b21 # codeql-bundle-v2.16.0 uses: github/codeql-action/init@592977e6ae857384aa79bb31e7a1d62d63449ec5 # codeql-bundle-v2.16.3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -54,4 +54,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e6a47e25652c5dbefe5b65d3b8e8dab0e9364b21 # codeql-bundle-v2.16.0 uses: github/codeql-action/analyze@592977e6ae857384aa79bb31e7a1d62d63449ec5 # codeql-bundle-v2.16.3
+4 -4
View File
@@ -14,7 +14,7 @@ jobs:
key: node-modules-${{ hashFiles('**/yarn.lock') }} key: node-modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: | restore-keys: |
node-modules- node-modules-
- uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
node-version: 16 node-version: 16
- run: yarn install - run: yarn install
@@ -32,7 +32,7 @@ jobs:
key: node-modules-${{ hashFiles('**/yarn.lock') }} key: node-modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: | restore-keys: |
node-modules- node-modules-
- uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
node-version: 16 node-version: 16
- run: yarn run compliance:licenses - run: yarn run compliance:licenses
@@ -51,7 +51,7 @@ jobs:
key: node-modules-${{ hashFiles('**/yarn.lock') }} key: node-modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: | restore-keys: |
node-modules- node-modules-
- uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
node-version: 16 node-version: 16
- run: yarn lint - run: yarn lint
@@ -68,7 +68,7 @@ jobs:
with: with:
path: '**/node_modules' path: '**/node_modules'
key: node-modules-${{ hashFiles('**/yarn.lock') }} key: node-modules-${{ hashFiles('**/yarn.lock') }}
- uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
node-version: 16 node-version: 16
- run: yarn test - run: yarn test
@@ -0,0 +1,58 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
/**
* Alias contains all fields related to an Alias resource
*/
export default class GeneratedAliasModel extends BaseModel {
// =attributes
@attr('string', {
description: 'The type of the resource, to help differentiate schemas',
})
type;
@attr('string', {
description: 'Optional name for identification purposes.',
})
name;
@attr('string', {
description: 'Optional user-set description for identification purposes.',
})
description;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
@attr('string', {
description: 'This is the id of the resource that this Alias points to.',
})
destination_id;
@attr('object', {
isNestedAttribute: true,
description:
'This contains a host_id which is the id of the host that the session will be authorized for.',
})
authorize_session_arguments;
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import GeneratedAliasModel from '../generated/models/alias';
export const TYPE_ALIAS_TARGET = 'target';
export const TYPES_ALIAS = Object.freeze([TYPE_ALIAS_TARGET]);
export default class AliasModel extends GeneratedAliasModel {}
+6
View File
@@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
export { default } from 'api/models/alias';
+16
View File
@@ -753,6 +753,22 @@ function routes() {
return policies.create(attrs); return policies.create(attrs);
}); });
// Alias
this.get(
'/aliases',
({ aliases }, { queryParams: { scope_id: scopeId } }) => {
return aliases.where({ scopeId });
},
);
this.get('/aliases/:id');
this.del('/aliases/:id');
this.patch('/aliases/:id');
this.post('/aliases', function ({ aliases }) {
const attrs = this.normalizedRequestAttrs();
return aliases.create(attrs);
});
// session recordings // session recordings
this.get( this.get(
'/session-recordings', '/session-recordings',
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import factory from '../generated/factories/alias';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
const destinationIDs = ['', generateId('t_')];
export default factory.extend({
id: () => generateId('alt_'),
destination_id: (i) => destinationIDs[i % destinationIDs.length],
authorized_actions: () =>
permissions.authorizedActionsFor('alias') || [
'no-op',
'read',
'update',
'delete',
],
});
+3 -2
View File
@@ -33,12 +33,13 @@ export default factory.extend({
roles: ['create', 'list'], roles: ['create', 'list'],
}; };
// Worker permissions only available on the global scope // Resources that are available only on the global scope
if (this.type === 'global') { if (this.type === 'global') {
collectionActions.workers = ['create:worker-led', 'list']; collectionActions.workers = ['create:worker-led', 'list'];
collectionActions.aliases = ['create', 'list'];
} }
// Session recording resources are only available on global or org scope // Resources that are only available on the global or the org scope
if (this.type === 'global' || this.type === 'org') { if (this.type === 'global' || this.type === 'org') {
collectionActions['storage-buckets'] = ['create', 'list']; collectionActions['storage-buckets'] = ['create', 'list'];
collectionActions['session-recordings'] = ['list']; collectionActions['session-recordings'] = ['list'];
@@ -0,0 +1,21 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
import { TYPE_ALIAS_TARGET } from 'api/models/alias';
/**
* GeneratedAliasModelFactory
*/
export default Factory.extend({
name: () => faker.word.words(),
type: TYPE_ALIAS_TARGET,
value: () => faker.word.words(),
description: () => faker.word.words(),
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
version: () => faker.number.int(),
});
+11
View File
@@ -0,0 +1,11 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { belongsTo } from 'miragejs';
import Model from './base';
export default Model.extend({
scope: belongsTo(),
});
+3
View File
@@ -108,6 +108,9 @@ export default function (server) {
server.createList('policy', 3, { scope: globalScope }); server.createList('policy', 3, { scope: globalScope });
server.createList('policy', 3, { scope: orgScope }); server.createList('policy', 3, { scope: orgScope });
// Aliases
server.createList('alias', 3, { scope: globalScope });
// Other resources // Other resources
server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => { server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => {
server.createList('host-catalog', 8, { scope }, 'withChildren'); server.createList('host-catalog', 8, { scope }, 'withChildren');
@@ -0,0 +1,17 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Model | alias', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let store = this.owner.lookup('service:store');
let model = store.createRecord('alias', {});
assert.ok(model);
});
});
@@ -999,3 +999,8 @@ policy:
label: Do not delete label: Do not delete
custom: custom:
label: Custom label: Custom
alias:
title: Alias
title_plural: Aliases
titles:
new: New Alias
@@ -12,7 +12,7 @@
<dropdown.button <dropdown.button
@style='danger' @style='danger'
@disabled={{@model.canSave}} @disabled={{@model.canSave}}
{{on 'click' (route-action 'deleteAccount' @model)}} {{on 'click' (fn @delete @model)}}
> >
{{t 'resources.account.actions.delete'}} {{t 'resources.account.actions.delete'}}
</dropdown.button> </dropdown.button>
@@ -12,7 +12,7 @@
<dropdown.button <dropdown.button
@style='danger' @style='danger'
@disabled={{@model.canSave}} @disabled={{@model.canSave}}
{{on 'click' (route-action 'deleteManagedGroup' @model)}} {{on 'click' (fn @delete @model)}}
> >
{{t 'resources.managed-group.actions.delete'}} {{t 'resources.managed-group.actions.delete'}}
</dropdown.button> </dropdown.button>
@@ -30,7 +30,7 @@
<dropdown.button <dropdown.button
@style='danger' @style='danger'
@disabled={{@model.canSave}} @disabled={{@model.canSave}}
{{on 'click' (route-action 'delete' @model)}} {{on 'click' @delete}}
> >
{{t 'resources.credential-store.actions.delete'}} {{t 'resources.credential-store.actions.delete'}}
</dropdown.button> </dropdown.button>
@@ -10,58 +10,48 @@
@showEditToggle={{if @model.isNew false true}} @showEditToggle={{if @model.isNew false true}}
as |form| as |form|
> >
<form.input <InfoField @value={{@model.type}} readonly={{true}} as |F|>
@value={{@model.type}} <F.Label>{{t 'form.type.label'}}</F.Label>
@label={{t 'form.type.label'}} </InfoField>
readonly={{true}}
@disabled={{true}}
/>
{{#unless @model.isNew}} {{#unless @model.isNew}}
<form.input
@value={{@model.issuer}} <InfoField @value={{@model.issuer}} readonly={{true}} as |F|>
@label={{t 'form.issuer.label'}} <F.Label>{{t 'form.issuer.label'}}</F.Label>
readonly={{true}} </InfoField>
@disabled={{true}}
/> <InfoField @value={{@model.subject}} readonly={{true}} as |F|>
<form.input <F.Label>{{t 'form.subject.label'}}</F.Label>
@value={{@model.subject}} </InfoField>
@label={{t 'form.subject.label'}}
readonly={{true}} <InfoField @value={{@model.email}} readonly={{true}} as |F|>
@disabled={{true}} <F.Label>{{t 'form.email.label'}}</F.Label>
/> </InfoField>
<form.input
@value={{@model.email}} <InfoField @value={{@model.full_name}} readonly={{true}} as |F|>
@label={{t 'form.email.label'}} <F.Label>{{t 'form.full_name.label'}}</F.Label>
readonly={{true}} </InfoField>
@disabled={{true}}
/>
<form.input
@value={{@model.full_name}}
@label={{t 'form.full_name.label'}}
readonly={{true}}
@disabled={{true}}
/>
{{/unless}} {{/unless}}
<form.input <Hds::Form::TextInput::Field
@name='name' name='name'
@type='name'
@value={{@model.name}} @value={{@model.name}}
@label={{t 'form.name.label'}} @isInvalid={{@model.errors.name}}
@helperText={{t 'form.name.help'}} @isOptional={{true}}
@error={{@model.errors.name}} disabled={{form.disabled}}
readonly={{false}} {{on 'input' (set-from-event @model 'name')}}
as |field| as |F|
> >
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
{{#if @model.errors.name}} {{#if @model.errors.name}}
<field.errors as |errors|> <F.Error as |E|>
{{#each @model.errors.name as |error|}} {{#each @model.errors.name as |error|}}
<errors.message>{{error.message}}</errors.message> <E.Message data-test-error-message-name>{{error.message}}</E.Message>
{{/each}} {{/each}}
</field.errors> </F.Error>
{{/if}} {{/if}}
</form.input> </Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field <Hds::Form::Textarea::Field
@isOptional={{true}} @isOptional={{true}}
@@ -83,41 +73,41 @@
</Hds::Form::Textarea::Field> </Hds::Form::Textarea::Field>
{{#if @model.isNew}} {{#if @model.isNew}}
<form.input <Hds::Form::TextInput::Field
@name='issuer' name='issuer'
@type='text'
@value={{@model.issuer}} @value={{@model.issuer}}
@label={{t 'form.issuer.label'}} @isInvalid={{@model.errors.issuer}}
@error={{@model.errors.issuer}} disabled={{form.disabled}}
readonly={{false}} {{on 'input' (set-from-event @model 'issuer')}}
as |field| as |F|
> >
<F.Label>{{t 'form.issuer.label'}}</F.Label>
{{#if @model.errors.issuer}} {{#if @model.errors.issuer}}
<field.errors as |errors|> <F.Error as |E|>
{{#each @model.errors.issuer as |error|}} {{#each @model.errors.issuer as |error|}}
<errors.message>{{error.message}}</errors.message> <E.Message>{{error.message}}</E.Message>
{{/each}} {{/each}}
</field.errors> </F.Error>
{{/if}} {{/if}}
</form.input> </Hds::Form::TextInput::Field>
<form.input <Hds::Form::TextInput::Field
@name='subject' name='subject'
@type='text'
@value={{@model.subject}} @value={{@model.subject}}
@label={{t 'form.subject.label'}} @isInvalid={{@model.errors.subject}}
@error={{@model.errors.subject}} disabled={{form.disabled}}
readonly={{false}} {{on 'input' (set-from-event @model 'subject')}}
as |field| as |F|
> >
<F.Label>{{t 'form.subject.label'}}</F.Label>
{{#if @model.errors.subject}} {{#if @model.errors.subject}}
<field.errors as |errors|> <F.Error as |E|>
{{#each @model.errors.subject as |error|}} {{#each @model.errors.subject as |error|}}
<errors.message>{{error.message}}</errors.message> <E.Message>{{error.message}}</E.Message>
{{/each}} {{/each}}
</field.errors> </F.Error>
{{/if}} {{/if}}
</form.input> </Hds::Form::TextInput::Field>
{{/if}} {{/if}}
{{#if (can 'save model' @model)}} {{#if (can 'save model' @model)}}
@@ -10,37 +10,35 @@
@showEditToggle={{if @model.isNew false true}} @showEditToggle={{if @model.isNew false true}}
as |form| as |form|
> >
<form.input <InfoField @value={{@model.type}} readonly={{true}} as |F|>
@value={{@model.type}} <F.Label>{{t 'form.type.label'}}</F.Label>
@label={{t 'form.type.label'}} </InfoField>
readonly={{true}}
@disabled={{true}}
/>
<form.input <Hds::Form::TextInput::Field
@name='name' name='name'
@type='name'
@value={{@model.name}} @value={{@model.name}}
@label={{t 'form.name.label'}} @isInvalid={{@model.errors.name}}
@helperText={{t 'form.name.help'}} @isOptional={{true}}
@error={{@model.errors.name}} disabled={{form.disabled}}
readonly={{false}} {{on 'input' (set-from-event @model 'name')}}
as |field| as |F|
> >
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
{{#if @model.errors.name}} {{#if @model.errors.name}}
<field.errors as |errors|> <F.Error as |E|>
{{#each @model.errors.name as |error|}} {{#each @model.errors.name as |error|}}
<errors.message>{{error.message}}</errors.message> <E.Message data-test-error-message-name>{{error.message}}</E.Message>
{{/each}} {{/each}}
</field.errors> </F.Error>
{{/if}} {{/if}}
</form.input> </Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field <Hds::Form::Textarea::Field
@isOptional={{true}} name='description'
@value={{@model.description}} @value={{@model.description}}
@isInvalid={{@model.errors.description}} @isInvalid={{@model.errors.description}}
name='description' @isOptional={{true}}
disabled={{form.disabled}} disabled={{form.disabled}}
as |F| as |F|
> >
@@ -55,43 +53,45 @@
{{/if}} {{/if}}
</Hds::Form::Textarea::Field> </Hds::Form::Textarea::Field>
<form.input <Hds::Form::TextInput::Field
@name='login_name' name='login_name'
@type='text'
@value={{@model.login_name}} @value={{@model.login_name}}
@label={{t 'form.login_name.label'}} @isInvalid={{@model.errors.login_name}}
@error={{@model.errors.login_name}} @isRequired={{true}}
readonly={{false}} disabled={{form.disabled}}
as |field| {{on 'input' (set-from-event @model 'login_name')}}
as |F|
> >
<F.Label>{{t 'form.login_name.label'}}</F.Label>
{{#if @model.errors.login_name}} {{#if @model.errors.login_name}}
<field.errors as |errors|> <F.Error as |E|>
{{#each @model.errors.login_name as |error|}} {{#each @model.errors.login_name as |error|}}
<errors.message>{{error.message}}</errors.message> <E.Message>{{error.message}}</E.Message>
{{/each}} {{/each}}
</field.errors> </F.Error>
{{/if}} {{/if}}
</form.input> </Hds::Form::TextInput::Field>
{{#if @model.isNew}} {{#if @model.isNew}}
<form.input <Hds::Form::TextInput::Field
@name='password' name='password'
@type='password' @type='password'
@value={{this.password}} @value={{@model.password}}
@label={{t 'form.password.label'}} @isInvalid={{@model.errors.password}}
@error={{@model.errors.password}} disabled={{form.disabled}}
readonly={{false}}
autocomplete='new-password' autocomplete='new-password'
as |field| {{on 'input' (set-from-event @model 'password')}}
as |F|
> >
<F.Label>{{t 'form.password.label'}}</F.Label>
{{#if @model.errors.password}} {{#if @model.errors.password}}
<field.errors as |errors|> <F.Error as |E|>
{{#each @model.errors.password as |error|}} {{#each @model.errors.password as |error|}}
<errors.message>{{error.message}}</errors.message> <E.Message>{{error.message}}</E.Message>
{{/each}} {{/each}}
</field.errors> </F.Error>
{{/if}} {{/if}}
</form.input> </Hds::Form::TextInput::Field>
{{/if}} {{/if}}
{{#if (can 'save model' @model)}} {{#if (can 'save model' @model)}}
@@ -18,7 +18,7 @@
<dropdown.button <dropdown.button
@style='danger' @style='danger'
@disabled={{@model.canSave}} @disabled={{@model.canSave}}
{{on 'click' (route-action 'delete' @model)}} {{on 'click' @delete}}
> >
{{t 'resources.group.actions.delete'}} {{t 'resources.group.actions.delete'}}
</dropdown.button> </dropdown.button>
@@ -5,9 +5,45 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class AccountChangePasswordController extends Controller { export default class AccountChangePasswordController extends Controller {
// =services // =services
@service session; @service session;
@service router;
// =actions
/**
* Changes the password on the account and then redirects to the index route.
* @param {AccountModel} account
* @param {string} currentPassword
* @param {string} newPassword
*/
@action
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.save-success')
async changePassword(account, currentPassword, newPassword) {
await account.changePassword(currentPassword, newPassword);
// Transition to index after success. We ignore errors in the transition,
// since the index route is responsible for additional
// redirects depending on the authentication context. These additional
// redirects may abort this initial transition, resulting in a
// TransitionAborted error, which we do not want to show to the user.
try {
await this.router.replaceWith('index');
} catch (e) {
// no-op
}
}
/**
* On cancel, redirects to index route for further processing.
*/
@action
cancel() {
this.router.replaceWith('index');
}
} }
+63 -1
View File
@@ -5,11 +5,73 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { getOwner } from '@ember/application';
import { action } from '@ember/object';
export default class ApplicationController extends Controller { export default class ApplicationController extends Controller {
// =services // =services
@service session; @service session;
@service flashMessages; @service features;
@service featureEdition; @service featureEdition;
@service flashMessages;
// =actions
/**
* Delegates invalidation to the session service.
*/
@action
invalidateSession() {
this.session.invalidate();
}
/**
* Applies the specified color theme to the root ember element.
* @param {string} theme - "light", "dark", or nullish (system default)
*/
@action
toggleTheme(theme) {
const rootElementSelector = getOwner(this).rootElement;
const rootEl = getOwner(this)
.lookup('service:-document')
.querySelector(rootElementSelector);
this.session.set('data.theme', theme);
switch (theme) {
case 'light':
rootEl.classList.add('rose-theme-light');
rootEl.classList.remove('rose-theme-dark');
break;
case 'dark':
rootEl.classList.add('rose-theme-dark');
rootEl.classList.remove('rose-theme-light');
break;
default:
rootEl.classList.remove('rose-theme-dark');
rootEl.classList.remove('rose-theme-light');
}
}
/**
* Switches to specified edition.
* Options are oss, enterprise, and hcp.
* @param {string} edition
*/
@action
toggleEdition(edition) {
this.featureEdition.setEdition(edition);
}
/**
*
* @param {string} feature
*/
@action
toggleFeature(feature) {
if (this.features.isEnabled(feature)) {
this.features.disable(feature);
} else {
this.features.enable(feature);
}
}
} }
@@ -0,0 +1,5 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeAliasesAliasIndexController extends Controller {
@controller('scopes/scope/aliases/index') aliases;
}
@@ -0,0 +1,5 @@
import Controller from '@ember/controller';
export default class ScopesScopeAliasesIndexController extends Controller {
// =services
}
@@ -0,0 +1,5 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeAliasesNewController extends Controller {
@controller('scopes/scope/aliases/index') aliases;
}
@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeAuthMethodsAuthMethodAccountsAccountIndexController extends Controller {
@controller('scopes/scope/auth-methods/auth-method/accounts/index') accounts;
}
@@ -0,0 +1,38 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Controller, { inject as controller } from '@ember/controller';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { loading } from 'ember-loading';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeAuthMethodsAuthMethodAccountsAccountSetPasswordController extends Controller {
// =controllers
@controller('scopes/scope/auth-methods/auth-method/accounts/index') accounts;
// =services
@service router;
// =actions
/**
* Set password for the specified account.
* @param {AccountModel} account
* @param {string} password
*/
@action
@loading
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.save-success')
async setPassword(account, password) {
await account.setPassword(password);
await this.router.replaceWith(
'scopes.scope.auth-methods.auth-method.accounts.account.set-password',
);
}
}
@@ -0,0 +1,82 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeAuthMethodsAuthMethodAccountsIndexController extends Controller {
// =services
@service can;
@service router;
// =actions
/**
* Rollback changes on an account.
* @param {AccountModel} account
*/
@action
async cancel(account) {
const { isNew } = account;
account.rollbackAttributes();
if (isNew) {
await this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.accounts',
);
}
}
/**
* Save an account in current scope.
* @param {AccountModel} account
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(account, password) {
const { isNew } = account;
const adapterOptions = {};
if (isNew) {
adapterOptions.password = password;
}
await account.save({ adapterOptions });
if (this.can.can('read model', account)) {
await this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.accounts.account',
account,
);
} else {
await this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.accounts',
);
}
await this.router.refresh();
}
/**
* Delete an account in current scope and redirect to index
* @param {AccountModel} account
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(account) {
await account.destroyRecord();
await this.router.replaceWith(
'scopes.scope.auth-methods.auth-method.accounts',
);
await this.router.refresh();
}
}
@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeAuthMethodsAuthMethodAccountsNewController extends Controller {
@controller('scopes/scope/auth-methods/auth-method/accounts/index') accounts;
}
@@ -0,0 +1,89 @@
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsIndexController extends Controller {
// =services
@service can;
@service router;
// =actions
/**
* Rollback changes on a managed group.
* @param {ManagedGroupModel} managedGroup
*/
@action
async cancel(managedGroup) {
const { isNew } = managedGroup;
managedGroup.rollbackAttributes();
if (isNew) {
await this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.managed-groups',
);
}
}
/**
* Save a managed group in current scope.
* @param {ManagedGroupModel} managedGroup
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(managedGroup) {
await managedGroup.save();
if (this.can.can('read model', managedGroup)) {
await this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.managed-groups.managed-group',
managedGroup,
);
} else {
await this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.managed-groups',
);
}
await this.router.refresh();
}
/**
* Delete a managed group in current scope and redirect to index
* @param {ManagedGroupModel} managed group
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(managedGroup) {
await managedGroup.destroyRecord();
await this.router.replaceWith(
'scopes.scope.auth-methods.auth-method.managed-groups',
);
await this.router.refresh();
}
/**
* Copies the contents of string array fields in order to force the instance
* into a dirty state. This ensures that `model.rollbackAttributes()` reverts
* to the original expected array.
*
* The deep copy implemented here is required to ensure that both the
* array itself and its members are all new.
*
* @param {managedGroupModel} managedGroup
*/
@action
edit(managedGroup) {
if (managedGroup.group_names) {
managedGroup.group_names = structuredClone(managedGroup.group_names);
}
}
}
@@ -0,0 +1,7 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsManagedGroupIndexController extends Controller {
@controller('scopes/scope/auth-methods/index') authMethods;
@controller('scopes/scope/auth-methods/auth-method/managed-groups/index')
managedGroups;
}
@@ -0,0 +1,6 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsManagedGroupMembersController extends Controller {
@controller('scopes/scope/auth-methods/auth-method/managed-groups/index')
managedGroups;
}
@@ -0,0 +1,7 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsNewController extends Controller {
@controller('scopes/scope/auth-methods/index') authMethods;
@controller('scopes/scope/auth-methods/auth-method/managed-groups/index')
managedGroups;
}
@@ -4,6 +4,7 @@
*/ */
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { action } from '@ember/object';
export default class ScopesScopeAuthMethodsIndexController extends Controller { export default class ScopesScopeAuthMethodsIndexController extends Controller {
// =attributes // =attributes
@@ -15,4 +16,35 @@ export default class ScopesScopeAuthMethodsIndexController extends Controller {
get hasPrimaryAuthMethod() { get hasPrimaryAuthMethod() {
return Boolean(this.scopeModel.primary_auth_method_id); return Boolean(this.scopeModel.primary_auth_method_id);
} }
// =actions
/**
* Removes an item from array `property` at `index` on the
* passed `authMethod`. This is used to manage entries in fragment array
* fields such as `signing_algorithms`.
* @param {AuthMethodModel} authMethod
* @param {string} property
* @param {number} index
*/
@action
async removeItemByIndex(authMethod, property, index) {
const array = authMethod.get(property).filter((item, i) => i !== index);
authMethod.set(property, array);
}
/**
* Adds a string item to array `property` on the passed `authMethod`.
* This is used to manage entries in fragment OIDC string array fields such
* as `signing_algorithms`.
* @param {AuthMethodModel} authMethod
* @param {string} property
* @param {string} value
*/
@action
async addStringItem(authMethod, property, value) {
const existingArray = authMethod[property] ?? [];
const array = [...existingArray, { value }];
authMethod.set(property, array);
}
} }
@@ -0,0 +1,8 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesCredentialLibraryIndexController extends Controller {
@controller(
'scopes/scope/credential-stores/credential-store/credential-libraries/index',
)
credentialLibraries;
}
@@ -0,0 +1,99 @@
import Controller, { inject as controller } from '@ember/controller';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { inject as service } from '@ember/service';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesIndexController extends Controller {
@controller('scopes/scope/credential-stores/index') credentialStores;
// =services
@service can;
@service router;
// =actions
/**
* Rollback changes on a credential library.
* @param {CredentialLibraryModel} credentialLibrary
*/
@action
async cancel(credentialLibrary) {
const { isNew } = credentialLibrary;
credentialLibrary.rollbackAttributes();
if (isNew)
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credential-libraries',
);
}
/**
* Handle save of a credential library
* @param {CredentialLibraryModel} credentialLibrary
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(credentialLibrary) {
await credentialLibrary.save();
if (this.can.can('read model', credentialLibrary)) {
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credential-libraries.credential-library',
credentialLibrary,
);
} else {
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credential-libraries',
);
}
await this.router.refresh();
}
/**
* Handle delete of a credential library
* @param {CredentialLibraryModel} credentialLibrary
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(credentialLibrary) {
await credentialLibrary.destroyRecord();
await this.router.replaceWith(
'scopes.scope.credential-stores.credential-store.credential-libraries',
);
await this.router.refresh();
}
/**
* Copies the contents of array fields in order to force the instance
* into a dirty state. This ensures that `model.rollbackAttributes()` reverts
* to the original expected array.
*
* The deep copy implemented here is required to ensure that both the
* array itself and its members are all new.
*
* @param {credentialLibraryModel} credentialLibrary
*/
@action
edit(credentialLibrary) {
const { critical_options, extensions } = credentialLibrary;
credentialLibrary.critical_options = structuredClone(critical_options);
credentialLibrary.extensions = structuredClone(extensions);
}
/**
* Update type of credential library
* @param {string} type
*/
@action
async changeType(type) {
await this.router.replaceWith({ queryParams: { type } });
}
}
@@ -3,10 +3,13 @@
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */
import Controller from '@ember/controller'; import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesNewController extends Controller { export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesNewController extends Controller {
// =services @controller(
'scopes/scope/credential-stores/credential-store/credential-libraries/index',
)
credentialLibraries;
// =attributes // =attributes
queryParams = ['type']; queryParams = ['type'];
@@ -0,0 +1,8 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialsCredentialIndexController extends Controller {
@controller(
'scopes/scope/credential-stores/credential-store/credentials/index',
)
credentials;
}
@@ -0,0 +1,81 @@
import Controller, { inject as controller } from '@ember/controller';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { inject as service } from '@ember/service';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialsIndexController extends Controller {
@controller('scopes/scope/credential-stores/index') credentialStores;
// =services
@service can;
@service router;
// =actions
/**
* Rollback changes on a credential.
* @param {CredentialModel} credential
*/
@action
async cancel(credential) {
const { isNew } = credential;
credential.rollbackAttributes();
if (isNew)
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credentials',
);
}
/**
* Handle save of a credential
* @param {CredentialModel} credential
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(credential) {
await credential.save();
if (this.can.can('read credential', credential)) {
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credentials.credential',
credential,
);
} else {
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credentials',
);
}
await this.router.refresh();
}
/**
* Handle delete of a credential
* @param {Credential} credential
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(credential) {
await credential.destroyRecord();
await this.router.replaceWith(
'scopes.scope.credential-stores.credential-store.credentials',
);
await this.router.refresh();
}
/**
* Update type of credential
* @param {string} type
*/
@action
async changeType(type) {
await this.router.replaceWith({ queryParams: { type } });
}
}
@@ -3,10 +3,13 @@
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */
import Controller from '@ember/controller'; import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialsNewController extends Controller { export default class ScopesScopeCredentialStoresCredentialStoreCredentialsNewController extends Controller {
// =services @controller(
'scopes/scope/credential-stores/credential-store/credentials/index',
)
credentials;
// =attributes // =attributes
@@ -0,0 +1,5 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeCredentialStoresCredentialStoreIndexController extends Controller {
@controller('scopes/scope/credential-stores/index') credentialStores;
}
@@ -0,0 +1,73 @@
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 ScopesScopeCredentialStoresIndexController extends Controller {
// =services
@service can;
@service router;
// =actions
/**
* Handle save
* @param {CredentialStoreModel} credentialStore
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess('notifications.save-success')
async save(credentialStore) {
await credentialStore.save();
if (this.can.can('read model', credentialStore)) {
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store',
credentialStore,
);
} else {
await this.router.transitionTo('scopes.scope.credential-stores');
}
await this.router.refresh();
}
/**
* Rollback changes on credential stores.
* @param {CredentialStoreModel} credentialStore
*/
@action
async cancel(credentialStore) {
const { isNew } = credentialStore;
credentialStore.rollbackAttributes();
if (isNew) {
await this.router.transitionTo('scopes.scope.credential-stores');
}
}
/**
* Deletes the credential store and redirects to index
* @param {CredentialStoreModel} credentialStore
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(credentialStore) {
await credentialStore.destroyRecord();
await this.router.replaceWith('scopes.scope.credential-stores');
await this.router.refresh();
}
/**
* Update type of credential store
* @param {string} type
*/
@action
async changeType(type) {
await this.router.replaceWith({ queryParams: { type } });
}
}
@@ -3,10 +3,10 @@
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */
import Controller from '@ember/controller'; import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeCredentialStoresNewController extends Controller { export default class ScopesScopeCredentialStoresNewController extends Controller {
// =services @controller('scopes/scope/credential-stores/index') credentialStores;
// =attributes // =attributes
queryParams = ['type']; queryParams = ['type'];
@@ -4,10 +4,16 @@
*/ */
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeGroupsGroupAddMembersController extends Controller { export default class ScopesScopeGroupsGroupAddMembersController extends Controller {
// =services // =services
@service router;
// =attributes // =attributes
/** /**
@@ -26,4 +32,29 @@ export default class ScopesScopeGroupsGroupAddMembersController extends Controll
const sorted = [global, ...orgsAndProjects].flat(); const sorted = [global, ...orgsAndProjects].flat();
return sorted; return sorted;
} }
// =actions
/**
* Adds members to the group and saves, replaces with the members index
* route, and notifies the user of success or error.
* @param {GroupModel} group
* @param {[string]} userIDs
*/
@action
@loading
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.add-success')
async addMembers(group, userIDs) {
await group.addMembers(userIDs);
await this.router.replaceWith('scopes.scope.groups.group.members');
}
/**
* Redirect to group members as if nothing ever happened.
*/
@action
async cancel() {
await this.router.replaceWith('scopes.scope.groups.group.members');
}
} }
@@ -0,0 +1,5 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeGroupsGroupIndexController extends Controller {
@controller('scopes/scope/groups/index') groups;
}
@@ -0,0 +1,5 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeGroupsGroupMembersController extends Controller {
@controller('scopes/scope/groups/index') groups;
}
@@ -0,0 +1,75 @@
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 ScopesScopeGroupsIndexController extends Controller {
// =services
@service can;
@service router;
// =actions
/**
* Rollback changes on a group.
* @param {GroupModel} group
*/
@action
async cancel(group) {
const { isNew } = group;
group.rollbackAttributes();
if (isNew) await this.router.transitionTo('scopes.scope.groups');
}
/**
* Save a group in current scope.
* @param {GroupModel} group
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(group) {
await group.save();
if (this.can.can('read model', group)) {
await this.router.transitionTo('scopes.scope.groups.group', group);
} else {
await this.router.transitionTo('scopes.scope.groups');
}
await this.router.refresh();
}
/**
* Delete a group in current scope and redirect to index
* @param {GroupModel} group
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(group) {
await group.destroyRecord();
await this.router.replaceWith('scopes.scope.groups');
await this.router.refresh();
}
/**
* Remove a member from the current role and redirect to members index.
* @param {GroupModel} group
* @param {UserModel} member
*/
@action
@loading
@confirm('questions.remove-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.remove-success')
async removeMember(group, member) {
await group.removeMember(member.id);
await this.router.refresh();
}
}
@@ -0,0 +1,5 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeGroupsNewController extends Controller {
@controller('scopes/scope/groups/index') groups;
}
+5
View File
@@ -95,6 +95,11 @@ Router.map(function () {
this.route('new'); this.route('new');
}); });
this.route('aliases', function () {
this.route('new');
this.route('alias', { path: ':alias_id' }, function () {});
});
this.route('host-catalogs', function () { this.route('host-catalogs', function () {
this.route('new'); this.route('new');
this.route('host-catalog', { path: ':host_catalog_id' }, function () { this.route('host-catalog', { path: ':host_catalog_id' }, function () {
@@ -5,8 +5,6 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { notifySuccess, notifyError } from 'core/decorators/notify';
/** /**
* The change password flow is associated only with password-type auth methods. * The change password flow is associated only with password-type auth methods.
@@ -30,33 +28,4 @@ export default class AccountChangePasswordRoute extends Route {
const account_id = this.session?.data?.authenticated?.account_id; const account_id = this.session?.data?.authenticated?.account_id;
return this.store.findRecord('account', account_id); return this.store.findRecord('account', account_id);
} }
// =actions
/**
* Changes the password on the account and then redirects to the index route.
* @param {AccountModel} account
* @param {string} currentPassword
* @param {string} newPassword
*/
@action
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.save-success')
async changePassword(account, currentPassword, newPassword) {
await account.changePassword(currentPassword, newPassword);
// Transition to index after success. We ignore errors in the transition,
// since the index route is responsible for additional
// redirects depending on the authentication context. These additional
// redirects may abort this initial transition, resulting in a
// TransitionAborted error, which we do not want to show to the user.
await this.router.replaceWith('index').catch(() => {});
}
/**
* On cancel, redirects to index route for further processing.
*/
@action
cancel() {
this.router.replaceWith('index');
}
} }
+13 -39
View File
@@ -19,8 +19,6 @@ export default class ApplicationRoute extends Route {
@service confirm; @service confirm;
@service router; @service router;
@service intl; @service intl;
@service features;
@service featureEdition;
// =attributes // =attributes
@@ -64,33 +62,6 @@ export default class ApplicationRoute extends Route {
this.toggleTheme(theme); this.toggleTheme(theme);
} }
// =actions
/**
* Delegates invalidation to the session service.
*/
@action
invalidateSession() {
this.session.invalidate();
}
/**
* Invalidates the session if a 401 error occurs and returns false to
* prevent further error handling.
* Returns true in all other cases, allowing error handling to occur (such
* as displaying the `error.hbs` template, if one exists).
* @param {Error} e
*/
@action
error(e) {
const isUnauthenticated = A(e?.errors)?.[0]?.isUnauthenticated;
if (isUnauthenticated) {
this.session.invalidate();
return false;
}
return true;
}
/** /**
* Applies the specified color theme to the root ember element. * Applies the specified color theme to the root ember element.
* @param {string} theme - "light", "dark", or nullish (system default) * @param {string} theme - "light", "dark", or nullish (system default)
@@ -117,17 +88,20 @@ export default class ApplicationRoute extends Route {
} }
} }
/**
* Invalidates the session if a 401 error occurs and returns false to
* prevent further error handling.
* Returns true in all other cases, allowing error handling to occur (such
* as displaying the `error.hbs` template, if one exists).
* @param {Error} e
*/
@action @action
toggleEdition(edition) { error(e) {
this.featureEdition.setEdition(edition); const isUnauthenticated = A(e?.errors)?.[0]?.isUnauthenticated;
} if (isUnauthenticated) {
this.session.invalidate();
@action return false;
toggleFeature(feature) {
if (this.features.isEnabled(feature)) {
this.features.disable(feature);
} else {
this.features.enable(feature);
} }
return true;
} }
} }
@@ -0,0 +1,42 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ScopesScopeAliasesRoute extends Route {
// =services
@service store;
@service session;
@service can;
@service router;
// =methods
/**
* If arriving here unauthenticated, redirect to index for further processing.
*/
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',
})
) {
return this.store.query('alias', { scope_id });
}
}
}
@@ -0,0 +1,25 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ScopesScopeAliasesAliasRoute extends Route {
@service store;
// =methods
/**
* Load an alias in current scope.
* @param {object} params
* @param {string} params.alias_id
* @return {Promise{AliasModel}}
*/
async model({ alias_id }) {
return this.store.findRecord('alias', alias_id, {
reload: true,
});
}
}
@@ -0,0 +1,16 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Route from '@ember/routing/route';
export default class ScopesScopeAliasesIndexRoute extends Route {
// =methods
setupController(controller) {
const scope = this.modelFor('scopes.scope');
super.setupController(...arguments);
controller.setProperties({ scope });
}
}
@@ -0,0 +1,43 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { TYPE_ALIAS_TARGET } from 'api/models/alias';
export default class ScopesScopeAliasesNewRoute extends Route {
// =services
@service store;
@service can;
@service router;
// =methods
/**
* Redirect to parent route when scope does not have create authorized action.
*/
beforeModel() {
const scopeModel = this.modelFor('scopes.scope');
if (
this.can.cannot('create model', scopeModel, { collection: 'aliases' })
) {
this.router.replaceWith('scopes.scope.aliases');
}
}
/**
* Creates a new unsaved alias.
* @return {AliasModel}
*/
model() {
const scopeModel = this.modelFor('scopes.scope');
const record = this.store.createRecord('alias', {
type: TYPE_ALIAS_TARGET,
});
record.scopeModel = scopeModel;
return record;
}
}
@@ -4,19 +4,13 @@
*/ */
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeAuthMethodsAuthMethodAccountsRoute extends Route { export default class ScopesScopeAuthMethodsAuthMethodAccountsRoute extends Route {
// =services // =services
@service store; @service store;
@service intl;
@service can; @service can;
@service router;
// =methods // =methods
@@ -42,67 +36,4 @@ export default class ScopesScopeAuthMethodsAuthMethodAccountsRoute extends Route
accounts, accounts,
}; };
} }
// =actions
/**
* Rollback changes on an account.
* @param {AccountModel} account
*/
@action
cancel(account) {
const { isNew } = account;
account.rollbackAttributes();
if (isNew)
this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.accounts',
);
}
/**
* Save an account in current scope.
* @param {AccountModel} account
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(account, password) {
const { isNew } = account;
const adapterOptions = {};
if (isNew) {
adapterOptions.password = password;
}
await account.save({ adapterOptions });
if (this.can.can('read model', account)) {
await this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.accounts.account',
account,
);
} else {
await this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.accounts',
);
}
this.refresh();
}
/**
* Delete an account in current scope and redirect to index
* @param {AccountModel} account
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async deleteAccount(account) {
await account.destroyRecord();
await this.router.replaceWith(
'scopes.scope.auth-methods.auth-method.accounts',
);
this.refresh();
}
} }
@@ -4,33 +4,5 @@
*/ */
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { loading } from 'ember-loading';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeAuthMethodsAuthMethodAccountsAccountSettingsRoute extends Route { export default class ScopesScopeAuthMethodsAuthMethodAccountsAccountSetPasswordRoute extends Route {}
// =services
@service intl;
@service router;
// =actions
/**
* Set password for the specified account.
* @param {AccountModel} account
* @param {string} password
*/
@action
@loading
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.save-success')
async setPassword(account, password) {
await account.setPassword(password);
await this.router.replaceWith(
'scopes.scope.auth-methods.auth-method.accounts.account.set-password',
);
}
}
@@ -4,19 +4,13 @@
*/ */
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsRoute extends Route { export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsRoute extends Route {
// =services // =services
@service store; @service store;
@service intl;
@service can; @service can;
@service router;
//=methods //=methods
@@ -43,62 +37,4 @@ export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsRoute extends
managedGroups, managedGroups,
}; };
} }
// =actions
/**
* Rollback changes on a managed group.
* @param {ManagedGroupModel} managedGroup
*/
@action
cancel(managedGroup) {
const { isNew } = managedGroup;
managedGroup.rollbackAttributes();
if (isNew) {
this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.managed-groups',
);
}
}
/**
* Save a managed group in current scope.
* @param {ManagedGroupModel} managedGroup
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(managedGroup) {
await managedGroup.save();
if (this.can.can('read model', managedGroup)) {
await this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.managed-groups.managed-group',
managedGroup,
);
} else {
await this.router.transitionTo(
'scopes.scope.auth-methods.auth-method.managed-groups',
);
}
this.refresh();
}
/**
* Delete a managed group in current scope and redirect to index
* @param {ManagedGroupModel} managed group
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async deleteManagedGroup(managedGroup) {
await managedGroup.destroyRecord();
await this.router.replaceWith(
'scopes.scope.auth-methods.auth-method.managed-groups',
);
this.refresh();
}
} }
@@ -4,14 +4,12 @@
*/ */
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsManagedGroupRoute extends Route { export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsManagedGroupRoute extends Route {
// =services // =services
@service store; @service store;
@service can;
@service router; @service router;
// =methods // =methods
@@ -44,21 +42,4 @@ export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsManagedGroupRo
); );
} }
} }
/**
* Copies the contents of string array fields in order to force the instance
* into a dirty state. This ensures that `model.rollbackAttributes()` reverts
* to the original expected array.
*
* The deep copy implemented here is required to ensure that both the
* array itself and its members are all new.
*
* @param {managedGroupModel} managedGroup
*/
@action
edit(managedGroup) {
if (managedGroup.group_names) {
managedGroup.group_names = structuredClone(managedGroup.group_names);
}
}
} }
@@ -5,17 +5,12 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { inject as service } from '@ember/service'; 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 ScopesScopeCredentialStoresRoute extends Route { export default class ScopesScopeCredentialStoresRoute extends Route {
// =services // =services
@service store; @service store;
@service can; @service can;
@service router;
// =methods // =methods
@@ -34,49 +29,4 @@ export default class ScopesScopeCredentialStoresRoute extends Route {
return this.store.query('credential-store', { scope_id }); return this.store.query('credential-store', { scope_id });
} }
} }
// =actions
/**
* Handle save
* @param {CredentialStoreModel} credentialStore
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess('notifications.save-success')
async save(credentialStore) {
await credentialStore.save();
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store',
credentialStore,
);
this.refresh();
}
/**
* Rollback changes on credential stores.
* @param {CredentialStoreModel} credentialStore
*/
@action
cancel(credentialStore) {
const { isNew } = credentialStore;
credentialStore.rollbackAttributes();
if (isNew) this.router.transitionTo('scopes.scope.credential-stores');
}
/**
* Deletes the credential store and redirects to index
* @param {CredentialStoreModel} credentialStore
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(credentialStore) {
await credentialStore.destroyRecord();
await this.router.replaceWith('scopes.scope.credential-stores');
this.refresh();
}
} }
@@ -4,10 +4,6 @@
*/ */
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesRoute extends Route { export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesRoute extends Route {
@@ -15,7 +11,6 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
@service store; @service store;
@service can; @service can;
@service router;
// =methods // =methods
@@ -36,62 +31,4 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
return this.store.query('credential-library', { credential_store_id }); return this.store.query('credential-library', { credential_store_id });
} }
} }
// =actions
/**
* Rollback changes on a credential library.
* @param {CredentialLibraryModel} credentialLibrary
*/
@action
cancel(credentialLibrary) {
const { isNew } = credentialLibrary;
credentialLibrary.rollbackAttributes();
if (isNew)
this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credential-libraries',
);
}
/**
* Handle save of a credential library
* @param {CredentialLibraryModel} credentialLibrary
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(credentialLibrary) {
await credentialLibrary.save();
if (this.can.can('read model', credentialLibrary)) {
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credential-libraries.credential-library',
credentialLibrary,
);
} else {
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credential-libraries',
);
}
this.refresh();
}
/**
* Handle delete of a credential library
* @param {CredentialLibraryModel} credentialLibrary
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async deleteCredentialLibrary(credentialLibrary) {
await credentialLibrary.destroyRecord();
await this.router.replaceWith(
'scopes.scope.credential-stores.credential-store.credential-libraries',
);
this.refresh();
}
} }
@@ -5,13 +5,11 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesCredentialLibraryRoute extends Route { export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesCredentialLibraryRoute extends Route {
// =services // =services
@service store; @service store;
@service can;
@service router; @service router;
// =methods // =methods
@@ -45,21 +43,4 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
); );
} }
} }
/**
* Copies the contents of array fields in order to force the instance
* into a dirty state. This ensures that `model.rollbackAttributes()` reverts
* to the original expected array.
*
* The deep copy implemented here is required to ensure that both the
* array itself and its members are all new.
*
* @param {credentialLibraryModel} credentialLibrary
*/
@action
edit(credentialLibrary) {
const { critical_options, extensions } = credentialLibrary;
credentialLibrary.critical_options = structuredClone(critical_options);
credentialLibrary.extensions = structuredClone(extensions);
}
} }
@@ -4,7 +4,6 @@
*/ */
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC } from 'api/models/credential-library'; import { TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC } from 'api/models/credential-library';
@@ -64,13 +63,4 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
credential_store_id, credential_store_id,
}); });
} }
/**
* Update type of credential library
* @param {string} type
*/
@action
changeType(type) {
this.router.replaceWith({ queryParams: { type } });
}
} }
@@ -4,10 +4,6 @@
*/ */
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialsRoute extends Route { export default class ScopesScopeCredentialStoresCredentialStoreCredentialsRoute extends Route {
@@ -15,7 +11,6 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialsRoute
@service store; @service store;
@service can; @service can;
@service router;
// =methods // =methods
@@ -36,61 +31,4 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialsRoute
return this.store.query('credential', { credential_store_id }); return this.store.query('credential', { credential_store_id });
} }
} }
// =actions
/**
* Rollback changes on a credential.
* @param {CredentialModel} credential
*/
@action
cancel(credential) {
const { isNew } = credential;
credential.rollbackAttributes();
if (isNew)
this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credentials',
);
}
/**
* Handle save of a credential
* @param {CredentialModel} credential
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(credential) {
await credential.save();
if (this.can.can('read model', credential)) {
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credentials.credential',
credential,
);
} else {
await this.router.transitionTo(
'scopes.scope.credential-stores.credential-store.credentials',
);
}
this.refresh();
}
/**
* Handle delete of a credential
* @param {Credential} credential
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async deleteCredential(credential) {
await credential.destroyRecord();
await this.router.replaceWith(
'scopes.scope.credential-stores.credential-store.credentials',
);
this.refresh();
}
} }
@@ -4,7 +4,6 @@
*/ */
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialsNewRoute extends Route { export default class ScopesScopeCredentialStoresCredentialStoreCredentialsNewRoute extends Route {
@@ -77,13 +76,4 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialsNewRou
...typeSpecificAttrs, ...typeSpecificAttrs,
}); });
} }
/**
* Update type of credential
* @param {string} type
*/
@action
changeType(type) {
this.router.replaceWith({ queryParams: { type } });
}
} }
@@ -4,7 +4,6 @@
*/ */
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
export default class ScopesScopeCredentialStoresNewRoute extends Route { export default class ScopesScopeCredentialStoresNewRoute extends Route {
@@ -63,13 +62,4 @@ export default class ScopesScopeCredentialStoresNewRoute extends Route {
record.scopeModel = scopeModel; record.scopeModel = scopeModel;
return record; return record;
} }
/**
* Update type of credential store
* @param {string} type
*/
@action
changeType(type) {
this.router.replaceWith({ queryParams: { type } });
}
} }
@@ -5,16 +5,11 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { inject as service } from '@ember/service'; 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 ScopesScopeGroupsRoute extends Route { export default class ScopesScopeGroupsRoute extends Route {
// =services // =services
@service store; @service store;
@service intl;
@service session; @service session;
@service can; @service can;
@service router; @service router;
@@ -39,51 +34,4 @@ export default class ScopesScopeGroupsRoute extends Route {
return this.store.query('group', { scope_id }); return this.store.query('group', { scope_id });
} }
} }
// =actions
/**
* Rollback changes on a group.
* @param {GroupModel} group
*/
@action
cancel(group) {
const { isNew } = group;
group.rollbackAttributes();
if (isNew) this.router.transitionTo('scopes.scope.groups');
}
/**
* Save a group in current scope.
* @param {GroupModel} group
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
async save(group) {
await group.save();
if (this.can.can('read model', group)) {
await this.router.transitionTo('scopes.scope.groups.group', group);
} else {
await this.router.transitionTo('scopes.scope.groups');
}
this.refresh();
}
/**
* Delete a group in current scope and redirect to index
* @param {GroupModel} group
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(group) {
await group.destroyRecord();
await this.router.replaceWith('scopes.scope.groups');
this.refresh();
}
} }
@@ -7,8 +7,6 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { hash } from 'rsvp'; import { hash } from 'rsvp';
import { loading } from 'ember-loading';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { resourceFilter } from 'core/decorators/resource-filter'; import { resourceFilter } from 'core/decorators/resource-filter';
export default class ScopesScopeGroupsGroupAddMembersRoute extends Route { export default class ScopesScopeGroupsGroupAddMembersRoute extends Route {
@@ -67,29 +65,6 @@ export default class ScopesScopeGroupsGroupAddMembersRoute extends Route {
// =actions // =actions
/**
* Adds members to the group and saves, replaces with the members index
* route, and notifies the user of success or error.
* @param {GroupModel} group
* @param {[string]} userIDs
*/
@action
@loading
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.add-success')
async addMembers(group, userIDs) {
await group.addMembers(userIDs);
await this.router.replaceWith('scopes.scope.groups.group.members');
}
/**
* Redirect to group members as if nothing ever happened.
*/
@action
cancel() {
this.router.replaceWith('scopes.scope.groups.group.members');
}
/** /**
* Sets the specified resource filter field to the specified value. * Sets the specified resource filter field to the specified value.
* @param {string} field * @param {string} field
@@ -4,12 +4,8 @@
*/ */
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { hash } from 'rsvp'; import { hash } from 'rsvp';
import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeGroupsGroupMembersRoute extends Route { export default class ScopesScopeGroupsGroupMembersRoute extends Route {
// =services // =services
@@ -37,21 +33,4 @@ export default class ScopesScopeGroupsGroupMembersRoute extends Route {
: [], : [],
}); });
} }
// =actions
/**
* Remove a member from the current role and redirect to members index.
* @param {GroupModel} group
* @param {UserModel} member
*/
@action
@loading
@confirm('questions.remove-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.remove-success')
async removeMember(group, member) {
await group.removeMember(member.id);
this.refresh();
}
} }
@@ -18,8 +18,8 @@
<page.body> <page.body>
<Form::Account::Password::ChangePassword <Form::Account::Password::ChangePassword
@model={{@model}} @model={{@model}}
@submit={{route-action 'changePassword' @model}} @submit={{fn this.changePassword @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.cancel @model}}
/> />
</page.body> </page.body>
+4 -4
View File
@@ -27,7 +27,7 @@
</dropdown.link> </dropdown.link>
<dropdown.separator /> <dropdown.separator />
<dropdown.button {{on 'click' (route-action 'invalidateSession')}}> <dropdown.button {{on 'click' this.invalidateSession}}>
{{t 'actions.deauthenticate'}} {{t 'actions.deauthenticate'}}
</dropdown.button> </dropdown.button>
@@ -41,7 +41,7 @@
this.session.data.theme this.session.data.theme
'system-default-theme' 'system-default-theme'
}} }}
@changed={{route-action 'toggleTheme'}} @changed={{this.toggleTheme}}
as |radioGroup| as |radioGroup|
> >
<dropdown.item> <dropdown.item>
@@ -83,7 +83,7 @@
<form.radioGroup <form.radioGroup
@name='edition' @name='edition'
@selectedValue={{this.featureEdition.edition}} @selectedValue={{this.featureEdition.edition}}
@changed={{route-action 'toggleEdition'}} @changed={{this.toggleEdition}}
as |radioGroup| as |radioGroup|
> >
{{#each this.featureEdition.editions as |edition|}} {{#each this.featureEdition.editions as |edition|}}
@@ -105,7 +105,7 @@
<form.checkbox <form.checkbox
@label={{feature}} @label={{feature}}
@checked={{feature-flag feature}} @checked={{feature-flag feature}}
@onChange={{route-action 'toggleFeature' feature}} @onChange={{fn this.toggleFeature feature}}
/> />
</dropdown.item> </dropdown.item>
{{/each}} {{/each}}
+6
View File
@@ -69,6 +69,12 @@
{{t 'resources.policy.title_plural'}} {{t 'resources.policy.title_plural'}}
</nav.link> </nav.link>
{{/if}} {{/if}}
{{#if (can 'navigate scope' @model collection='aliases')}}
<nav.link @route='scopes.scope.aliases'>
<Rose::Icon @name='flight-icons/svg/at-sign-16' @size='medium' />
{{t 'resources.alias.title_plural'}}
</nav.link>
{{/if}}
{{#if {{#if
(and (and
(can 'navigate worker' @model collection='workers') (can 'navigate worker' @model collection='workers')
@@ -0,0 +1,12 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
{{page-title (t 'resources.alias.title_plural')}}
<Breadcrumbs::Item
@text={{t 'resources.alias.title_plural'}}
@route='scopes.scope.aliases'
/>
{{outlet}}
@@ -0,0 +1,13 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
{{page-title @model.displayName}}
<Breadcrumbs::Item
@text={{@model.displayName}}
@route='scopes.scope.aliases.alias'
@model={{@model}}
/>
{{outlet}}
@@ -0,0 +1,11 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<Rose::Layout::Page as |page|>
<page.breadcrumbs>
<Breadcrumbs::Container />
</page.breadcrumbs>
</Rose::Layout::Page>
@@ -0,0 +1,27 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<Rose::Layout::Page as |page|>
<page.breadcrumbs>
<Breadcrumbs::Container />
</page.breadcrumbs>
<page.header>
<h2>
{{t 'resources.alias.title'}}
{{! TODO: Add doc link for alias }}
</h2>
{{! TODO: Add desc for alias }}
</page.header>
<page.actions>
{{#if (can 'create model' this.scope collection='aliases')}}
{{#if @model}}
<Hds::Button
@route='scopes.scope.aliases.new'
@text={{t 'resources.alias.titles.new'}}
/>
{{/if}}
{{/if}}
</page.actions>
</Rose::Layout::Page>
@@ -0,0 +1,27 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
{{page-title (t 'titles.new')}}
<Breadcrumbs::Item
@text={{t 'resources.alias.titles.new'}}
@route='scopes.scope.aliases.new'
/>
<Rose::Layout::Page as |page|>
<page.breadcrumbs>
<Breadcrumbs::Container />
</page.breadcrumbs>
<page.header>
<h2>
{{t 'resources.alias.titles.new'}}
{{! TODO: Add doc link for alias }}
</h2>
{{! TODO: Add desc for alias }}
</page.header>
</Rose::Layout::Page>
@@ -18,14 +18,17 @@
</page.navigation> </page.navigation>
<page.actions> <page.actions>
<AuthMethods::AuthMethod::Accounts::Account::Actions @model={{@model}} /> <AuthMethods::AuthMethod::Accounts::Account::Actions
@model={{@model}}
@delete={{this.accounts.delete}}
/>
</page.actions> </page.actions>
<page.body> <page.body>
<Form::Account <Form::Account
@model={{@model}} @model={{@model}}
@submit={{route-action 'save' @model}} @submit={{fn this.accounts.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.accounts.cancel @model}}
/> />
</page.body> </page.body>
@@ -26,14 +26,17 @@
</page.navigation> </page.navigation>
<page.actions> <page.actions>
<AuthMethods::AuthMethod::Accounts::Account::Actions @model={{@model}} /> <AuthMethods::AuthMethod::Accounts::Account::Actions
@model={{@model}}
@delete={{this.accounts.delete}}
/>
</page.actions> </page.actions>
<page.body> <page.body>
<Form::Account::Password::SetPassword <Form::Account::Password::SetPassword
@model={{@model}} @model={{@model}}
@submit={{route-action 'setPassword' @model}} @submit={{fn this.setPassword @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.accounts.cancel @model}}
/> />
</page.body> </page.body>
@@ -26,8 +26,8 @@
<page.body> <page.body>
<Form::Account <Form::Account
@model={{@model}} @model={{@model}}
@submit={{route-action 'save' @model}} @submit={{fn this.accounts.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.accounts.cancel @model}}
/> />
</page.body> </page.body>
@@ -24,17 +24,18 @@
<page.actions> <page.actions>
<AuthMethods::AuthMethod::ManagedGroups::ManagedGroup::Actions <AuthMethods::AuthMethod::ManagedGroups::ManagedGroup::Actions
@model={{@model}} @model={{@model}}
@delete={{this.managedGroups.delete}}
/> />
</page.actions> </page.actions>
<page.body> <page.body>
<Form::ManagedGroup <Form::ManagedGroup
@model={{@model}} @model={{@model}}
@edit={{route-action 'edit' @model}} @edit={{fn this.managedGroups.edit @model}}
@submit={{route-action 'save' @model}} @submit={{fn this.managedGroups.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.managedGroups.cancel @model}}
@removeItemByIndex={{route-action 'removeItemByIndex' @model}} @removeItemByIndex={{fn this.authMethods.removeItemByIndex @model}}
@addStringItem={{route-action 'addStringItem' @model}} @addStringItem={{fn this.authMethods.addStringItem @model}}
/> />
</page.body> </page.body>
@@ -30,6 +30,7 @@
<page.actions> <page.actions>
<AuthMethods::AuthMethod::ManagedGroups::ManagedGroup::Actions <AuthMethods::AuthMethod::ManagedGroups::ManagedGroup::Actions
@model={{@model.managedGroup}} @model={{@model.managedGroup}}
@delete={{this.managedGroups.delete}}
/> />
</page.actions> </page.actions>
@@ -26,10 +26,10 @@
<page.body> <page.body>
<Form::ManagedGroup <Form::ManagedGroup
@model={{@model}} @model={{@model}}
@submit={{route-action 'save' @model}} @submit={{fn this.managedGroups.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.managedGroups.cancel @model}}
@removeItemByIndex={{route-action 'removeItemByIndex' @model}} @removeItemByIndex={{fn this.authMethods.removeItemByIndex @model}}
@addStringItem={{route-action 'addStringItem' @model}} @addStringItem={{fn this.authMethods.addStringItem @model}}
/> />
</page.body> </page.body>
@@ -38,7 +38,7 @@
<dropdown.button <dropdown.button
@style='danger' @style='danger'
@disabled={{@model.canSave}} @disabled={{@model.canSave}}
{{on 'click' (route-action 'deleteCredentialLibrary' @model)}} {{on 'click' (fn this.credentialLibraries.delete @model)}}
> >
{{t 'resources.credential-library.actions.delete'}} {{t 'resources.credential-library.actions.delete'}}
</dropdown.button> </dropdown.button>
@@ -49,9 +49,9 @@
<page.body> <page.body>
<Form::CredentialLibrary <Form::CredentialLibrary
@model={{@model}} @model={{@model}}
@edit={{route-action 'edit' @model}} @edit={{fn this.credentialLibraries.edit @model}}
@submit={{route-action 'save' @model}} @submit={{fn this.credentialLibraries.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.credentialLibraries.cancel @model}}
/> />
</page.body> </page.body>
</Rose::Layout::Page> </Rose::Layout::Page>
@@ -22,6 +22,7 @@
<page.actions> <page.actions>
<CredentialStores::CredentialStore::Actions <CredentialStores::CredentialStore::Actions
@model={{this.credentialStore}} @model={{this.credentialStore}}
@delete={{fn this.credentialStores.delete this.credentialStore}}
/> />
</page.actions> </page.actions>
@@ -26,9 +26,9 @@
<page.body> <page.body>
<Form::CredentialLibrary <Form::CredentialLibrary
@model={{@model}} @model={{@model}}
@submit={{route-action 'save' @model}} @submit={{fn this.credentialLibraries.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.credentialLibraries.cancel @model}}
@changeType={{route-action 'changeType'}} @changeType={{this.credentialLibraries.changeType}}
/> />
</page.body> </page.body>
</Rose::Layout::Page> </Rose::Layout::Page>
@@ -38,7 +38,7 @@
<dropdown.button <dropdown.button
@style='danger' @style='danger'
@disabled={{@model.canSave}} @disabled={{@model.canSave}}
{{on 'click' (route-action 'deleteCredential' @model)}} {{on 'click' (fn this.credentials.delete @model)}}
> >
{{t 'resources.credential.actions.delete'}} {{t 'resources.credential.actions.delete'}}
</dropdown.button> </dropdown.button>
@@ -49,8 +49,8 @@
<page.body> <page.body>
<Form::Credential <Form::Credential
@model={{@model}} @model={{@model}}
@submit={{route-action 'save' @model}} @submit={{fn this.credentials.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.credentials.cancel @model}}
/> />
</page.body> </page.body>
</Rose::Layout::Page> </Rose::Layout::Page>
@@ -22,6 +22,7 @@
<page.actions> <page.actions>
<CredentialStores::CredentialStore::Actions <CredentialStores::CredentialStore::Actions
@model={{this.credentialStore}} @model={{this.credentialStore}}
@delete={{fn this.credentialStores.delete this.credentialStore}}
/> />
</page.actions> </page.actions>
@@ -26,9 +26,9 @@
<page.body> <page.body>
<Form::Credential <Form::Credential
@model={{@model}} @model={{@model}}
@submit={{route-action 'save' @model}} @submit={{fn this.credentials.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.credentials.cancel @model}}
@changeType={{route-action 'changeType'}} @changeType={{this.credentials.changeType}}
/> />
</page.body> </page.body>
</Rose::Layout::Page> </Rose::Layout::Page>
@@ -18,14 +18,17 @@
</page.navigation> </page.navigation>
<page.actions> <page.actions>
<CredentialStores::CredentialStore::Actions @model={{@model}} /> <CredentialStores::CredentialStore::Actions
@model={{@model}}
@delete={{fn this.credentialStores.delete @model}}
/>
</page.actions> </page.actions>
<page.body> <page.body>
<Form::CredentialStore <Form::CredentialStore
@model={{@model}} @model={{@model}}
@submit={{route-action 'save' @model}} @submit={{fn this.credentialStores.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.credentialStores.cancel @model}}
/> />
</page.body> </page.body>
</Rose::Layout::Page> </Rose::Layout::Page>
@@ -25,9 +25,9 @@
<page.body> <page.body>
<Form::CredentialStore <Form::CredentialStore
@model={{@model}} @model={{@model}}
@submit={{route-action 'save' @model}} @submit={{fn this.credentialStores.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.credentialStores.cancel @model}}
@changeType={{route-action 'changeType'}} @changeType={{this.credentialStores.changeType}}
/> />
</page.body> </page.body>
@@ -86,8 +86,8 @@
<Form::Group::AddMembers <Form::Group::AddMembers
@model={{@model.group}} @model={{@model.group}}
@users={{@model.users}} @users={{@model.users}}
@submit={{route-action 'addMembers' @model.group}} @submit={{fn this.addMembers @model.group}}
@cancel={{route-action 'cancel'}} @cancel={{this.cancel}}
/> />
</page.body> </page.body>
@@ -18,14 +18,17 @@
</page.navigation> </page.navigation>
<page.actions> <page.actions>
<Groups::Group::Actions @model={{@model}} /> <Groups::Group::Actions
@model={{@model}}
@delete={{fn this.groups.delete @model}}
/>
</page.actions> </page.actions>
<page.body> <page.body>
<Form::Group <Form::Group
@model={{@model}} @model={{@model}}
@submit={{route-action 'save' @model}} @submit={{fn this.groups.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.groups.cancel @model}}
/> />
</page.body> </page.body>
@@ -24,7 +24,10 @@
</page.navigation> </page.navigation>
<page.actions> <page.actions>
<Groups::Group::Actions @model={{@model.group}} /> <Groups::Group::Actions
@model={{@model.group}}
@delete={{fn this.groups.delete @model.group}}
/>
</page.actions> </page.actions>
<page.body> <page.body>
@@ -75,7 +78,7 @@
@text={{t 'actions.remove'}} @text={{t 'actions.remove'}}
{{on {{on
'click' 'click'
(route-action 'removeMember' @model.group B.data) (fn this.groups.removeMember @model.group B.data)
}} }}
/> />
</Hds::Dropdown> </Hds::Dropdown>
@@ -26,8 +26,8 @@
<page.body> <page.body>
<Form::Group <Form::Group
@model={{@model}} @model={{@model}}
@submit={{route-action 'save' @model}} @submit={{fn this.groups.save @model}}
@cancel={{route-action 'cancel' @model}} @cancel={{fn this.groups.cancel @model}}
/> />
</page.body> </page.body>
@@ -134,18 +134,18 @@ module('Acceptance | accounts | create', function (hooks) {
const accountsCount = this.server.db.accounts.length; const accountsCount = this.server.db.accounts.length;
await visit(urls.newAccount); await visit(urls.newAccount);
await fillIn('[name="name"]', 'Account name'); await fillIn('[name="name"]', 'Account name');
await click('form button:not([type="submit"])'); await click('.rose-form-actions [type="button"]');
assert.strictEqual(this.server.db.accounts.length, accountsCount);
assert.strictEqual(currentURL(), urls.accounts); assert.strictEqual(currentURL(), urls.accounts);
assert.strictEqual(this.server.db.accounts.length, accountsCount);
}); });
test('can cancel a new LDAP account creation', async function (assert) { test('can cancel a new LDAP account creation', async function (assert) {
const accountsCount = this.server.db.accounts.length; const accountsCount = this.server.db.accounts.length;
await visit(urls.newAccount); await visit(urls.newAccount);
await fillIn('[name="name"]', 'Account name'); await fillIn('[name="name"]', 'Account name');
await click('form button:not([type="submit"])'); await click('.rose-form-actions [type="button"]');
assert.strictEqual(this.server.db.accounts.length, accountsCount);
assert.strictEqual(currentURL(), urls.accounts); assert.strictEqual(currentURL(), urls.accounts);
assert.strictEqual(this.server.db.accounts.length, accountsCount);
}); });
test('saving a new account with invalid fields displays error messages', async function (assert) { test('saving a new account with invalid fields displays error messages', async function (assert) {
@@ -178,7 +178,7 @@ module('Acceptance | accounts | create', function (hooks) {
'Displays primary error message.', 'Displays primary error message.',
); );
assert.strictEqual( assert.strictEqual(
find('.rose-form-error-message').textContent.trim(), find('[data-test-error-message-name]').textContent.trim(),
'Name is required.', 'Name is required.',
'Displays field-level errors.', 'Displays field-level errors.',
); );
@@ -158,7 +158,7 @@ module('Acceptance | accounts | update', function (hooks) {
'Displays primary error message.', 'Displays primary error message.',
); );
assert.strictEqual( assert.strictEqual(
find('.rose-form-error-message').textContent.trim(), find('[data-test-error-message-name]').textContent.trim(),
'Name is required.', 'Name is required.',
'Displays field-level errors.', 'Displays field-level errors.',
); );
@@ -23,6 +23,8 @@ module(
setupApplicationTest(hooks); setupApplicationTest(hooks);
setupMirage(hooks); setupMirage(hooks);
let featuresService;
const instances = { const instances = {
scopes: { scopes: {
global: null, global: null,
@@ -83,6 +85,8 @@ module(
urls.usernamePasswordCredential = `${urls.credentials}/${instances.usernamePasswordCredential.id}`; urls.usernamePasswordCredential = `${urls.credentials}/${instances.usernamePasswordCredential.id}`;
urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`; urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`;
urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`; urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`;
featuresService = this.owner.lookup('service:features');
authenticateSession({}); authenticateSession({});
}); });
@@ -117,6 +121,7 @@ module(
}); });
test('can save changes to existing JSON credential', async function (assert) { test('can save changes to existing JSON credential', async function (assert) {
featuresService.enable('json-credentials');
const mockInput = 'random string'; const mockInput = 'random string';
assert.notEqual(instances.jsonCredential.name, mockInput); assert.notEqual(instances.jsonCredential.name, mockInput);
await visit(urls.jsonCredential); await visit(urls.jsonCredential);
+90 -30
View File
@@ -20,18 +20,25 @@ exports.checkBoundaryCli = async () => {
/** /**
* Uses the boundary CLI to authenticate to the specified Boundary instance * Uses the boundary CLI to authenticate to the specified Boundary instance
* @param {string} addr Address of the Boundary instance to be authenticated to
* @param {string} authMethodId ID of the auth method to be used for authentication
* @param {string} loginName Login name to be used for authentication
* @param {string} password Password to be used for authentication
*/ */
exports.authenticateBoundaryCli = async () => { exports.authenticateBoundaryCli = async (
addr,
authMethodId,
loginName,
password,
) => {
try { try {
execSync( execSync(
'boundary authenticate password' + 'boundary authenticate password' +
' -addr=' + ` -addr=${addr}` +
process.env.BOUNDARY_ADDR + ` -auth-method-id=${authMethodId}` +
' -auth-method-id=' + ` -login-name=${loginName}` +
process.env.E2E_PASSWORD_AUTH_METHOD_ID + ' -password=env://BPASS',
' -login-name=' + { env: { ...process.env, BPASS: password } },
process.env.E2E_PASSWORD_ADMIN_LOGIN_NAME +
' -password=env://E2E_PASSWORD_ADMIN_PASSWORD',
); );
} catch (e) { } catch (e) {
console.log(`${e.stderr}`); console.log(`${e.stderr}`);
@@ -41,25 +48,24 @@ exports.authenticateBoundaryCli = async () => {
/** /**
* Uses the boundary CLI to connect to the specified target * Uses the boundary CLI to connect to the specified target
* @param {string} targetId ID of the target to be connected to * @param {string} targetId ID of the target to be connected to
* @param {string} sshUser User to be used for the ssh connection
* @param {string} sshKeyPath Path to the ssh key to be used for the ssh connection
* @returns ChildProcess representing the result of the command execution * @returns ChildProcess representing the result of the command execution
*/ */
exports.connectToTarget = async (targetId) => { exports.connectToTarget = async (targetId, sshUser, sshKeyPath) => {
let connect; let connect;
try { try {
connect = exec( connect = exec(
'boundary connect' + 'boundary connect' +
' -target-id=' + ` -target-id=${targetId}` +
targetId + ' -exec /usr/bin/ssh --' +
' -exec /usr/bin/ssh --' + ` -l ${sshUser}` +
' -l ' + ` -i ${sshKeyPath}` +
process.env.E2E_SSH_USER + ' -o UserKnownHostsFile=/dev/null' +
' -i ' + ' -o StrictHostKeyChecking=no' +
process.env.E2E_SSH_KEY_PATH + ' -o IdentitiesOnly=yes' + // forces the use of the provided key
' -o UserKnownHostsFile=/dev/null' + ' -p {{boundary.port}}' +
' -o StrictHostKeyChecking=no' + ' {{boundary.ip}}',
' -o IdentitiesOnly=yes' + // forces the use of the provided key
' -p {{boundary.port}}' +
' {{boundary.ip}}',
); );
} catch (e) { } catch (e) {
console.log(`${e.stderr}`); console.log(`${e.stderr}`);
@@ -77,12 +83,12 @@ exports.connectSshToTarget = async (targetId) => {
try { try {
connect = exec( connect = exec(
'boundary connect ssh' + 'boundary connect ssh' +
' -target-id=' + ' -target-id=' +
targetId + targetId +
' --' + ' --' +
' -o UserKnownHostsFile=/dev/null' + ' -o UserKnownHostsFile=/dev/null' +
' -o StrictHostKeyChecking=no' + ' -o StrictHostKeyChecking=no' +
' -o IdentitiesOnly=yes', // forces the use of the provided key ' -o IdentitiesOnly=yes', // forces the use of the provided key
); );
} catch (e) { } catch (e) {
console.log(`${e.stderr}`); console.log(`${e.stderr}`);
@@ -124,6 +130,30 @@ exports.deleteOrgCli = async (orgId) => {
} }
}; };
/**
* Uses the boundary CLI to delete the specified storage bucket
* @param {string} storageBucketId ID of the storage bucket to be deleted
*/
exports.deleteStorageBucketCli = async (storageBucketId) => {
try {
exec('boundary storage-buckets delete -id=' + storageBucketId);
} catch (e) {
console.log(`${e.stderr}`);
}
};
/**
* Uses the boundary CLI to delete the specified policy
* @param {string} policyId ID of the policy to be deleted
*/
exports.deletePolicyCli = async (policyId) => {
try {
exec('boundary policies delete -id=' + policyId);
} catch (e) {
console.log(`${e.stderr}`);
}
};
/** /**
* Uses the boundary CLI to create a new project. * Uses the boundary CLI to create a new project.
* @param {string} orgId ID of the organization under which the project will be created. * @param {string} orgId ID of the organization under which the project will be created.
@@ -217,7 +247,6 @@ exports.makeAuthMethodPrimaryCli = async (scopeId, authMethodId) => {
exports.createNewPasswordAccountCli = async (authMethodId) => { exports.createNewPasswordAccountCli = async (authMethodId) => {
let passwordAccount; let passwordAccount;
const login = 'test-login'; const login = 'test-login';
process.env.ACCOUNT_PASSWORD = 'test-password';
try { try {
passwordAccount = JSON.parse( passwordAccount = JSON.parse(
execSync( execSync(
@@ -226,6 +255,7 @@ exports.createNewPasswordAccountCli = async (authMethodId) => {
-login-name ${login} \ -login-name ${login} \
-password env://ACCOUNT_PASSWORD \ -password env://ACCOUNT_PASSWORD \
-format json`, -format json`,
{ env: { ...process.env, ACCOUNT_PASSWORD: 'test-password' } },
), ),
).item; ).item;
} catch (e) { } catch (e) {
@@ -427,12 +457,14 @@ exports.createNewStaticCredentialStoreCli = async (projectId) => {
/** /**
* Uses the boundary CLI to create a new vault credential store. * Uses the boundary CLI to create a new vault credential store.
* @param {string} projectId ID of the project under which the credential store will be created. * @param {string} projectId ID of the project under which the credential store will be created.
* @param {string} vaultAddr Address of the vault that the credential store will be created for.
* @param {string} secretPolicyName Name of the secret policy that's used to create vault token. * @param {string} secretPolicyName Name of the secret policy that's used to create vault token.
* @param {string} boundaryPolicyName Name of the boundary policy that's used to create vault token. * @param {string} boundaryPolicyName Name of the boundary policy that's used to create vault token.
* @returns {Promise<string>} new credential store's ID * @returns {Promise<string>} new credential store's ID
*/ */
exports.createNewVaultCredentialStoreCli = async ( exports.createNewVaultCredentialStoreCli = async (
projectId, projectId,
vaultAddr,
secretPolicyName, secretPolicyName,
boundaryPolicyName, boundaryPolicyName,
) => { ) => {
@@ -463,7 +495,7 @@ exports.createNewVaultCredentialStoreCli = async (
`boundary credential-stores create vault \ `boundary credential-stores create vault \
-scope-id ${projectId} \ -scope-id ${projectId} \
-name ${credentialStoreName} \ -name ${credentialStoreName} \
-vault-address ${process.env.E2E_VAULT_ADDR} \ -vault-address ${vaultAddr} \
-vault-token ${clientToken} \ -vault-token ${clientToken} \
-format json`, -format json`,
), ),
@@ -482,7 +514,6 @@ exports.createNewVaultCredentialStoreCli = async (
exports.createNewUsernamePasswordCredentialCli = async (credentialStoreId) => { exports.createNewUsernamePasswordCredentialCli = async (credentialStoreId) => {
let usernamePasswordCredential; let usernamePasswordCredential;
const login = 'test-login'; const login = 'test-login';
process.env.CREDENTIALS_PASSWORD = 'credentials-password';
try { try {
usernamePasswordCredential = JSON.parse( usernamePasswordCredential = JSON.parse(
execSync( execSync(
@@ -492,6 +523,9 @@ exports.createNewUsernamePasswordCredentialCli = async (credentialStoreId) => {
-username ${login} \ -username ${login} \
-password env://CREDENTIALS_PASSWORD \ -password env://CREDENTIALS_PASSWORD \
-format json`, -format json`,
{
env: { ...process.env, CREDENTIALS_PASSWORD: 'credentials-password' },
},
), ),
).item; ).item;
} catch (e) { } catch (e) {
@@ -580,3 +614,29 @@ exports.getSessionCli = async (orgName, projectName, targetName) => {
} }
return session; return session;
}; };
/**
* Uses the boundary CLI to wait for a session recording to be available.
* @param {string} storageBucketId ID of storage bucket that the session recording is associated with
* @returns An object representing a session recording
*/
exports.waitForSessionRecordingCli = async (storageBucketId) => {
let i = 0;
let filteredSessionRecording = [];
do {
i = i + 1;
const sessionRecordings = JSON.parse(
execSync('boundary session-recordings list -format json'),
);
filteredSessionRecording = sessionRecordings.items.filter(
(obj) =>
obj.storage_bucket_id == storageBucketId && obj.state == 'available',
);
if (filteredSessionRecording.length > 0) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
} while (i < 5);
return filteredSessionRecording[0];
};
+319 -23
View File
@@ -118,15 +118,16 @@ exports.createNewHostSet = async (page) => {
/** /**
* Uses the UI to create a new host in a host set. Assumes you have just created a new host set. * Uses the UI to create a new host in a host set. Assumes you have just created a new host set.
* @param {Page} page Playwright page object * @param {Page} page Playwright page object
* @param {string} address Address of the host
* @returns Name of the host * @returns Name of the host
*/ */
exports.createNewHostInHostSet = async (page) => { exports.createNewHostInHostSet = async (page, address) => {
const hostName = 'Host ' + nanoid(); const hostName = 'Host ' + nanoid();
await page.getByText('Manage').click(); await page.getByText('Manage').click();
await page.getByRole('link', { name: 'Create and Add Host' }).click(); await page.getByRole('link', { name: 'Create and Add Host' }).click();
await page.getByLabel('Name').fill(hostName); await page.getByLabel('Name').fill(hostName);
await page.getByLabel('Description').fill('This is an automated test'); await page.getByLabel('Description').fill('This is an automated test');
await page.getByLabel('Address').fill(process.env.E2E_TARGET_ADDRESS); await page.getByLabel('Address').fill(address);
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await expect( await expect(
page.getByRole('alert').getByText('Success', { exact: true }), page.getByRole('alert').getByText('Success', { exact: true }),
@@ -138,7 +139,7 @@ exports.createNewHostInHostSet = async (page) => {
}; };
/** /**
* Uses the UI to create a credential store. Assumes you have selected the desired project. * Uses the UI to create a static credential store. Assumes you have selected the desired project.
* @param {Page} page Playwright page object * @param {Page} page Playwright page object
* @returns Name of the credential store * @returns Name of the credential store
*/ */
@@ -166,12 +167,47 @@ exports.createStaticCredentialStore = async (page) => {
return credentialStoreName; return credentialStoreName;
}; };
/**
* Uses the UI to create a vault credential store. Assumes you have selected the desired project.
* @param {Page} page Playwright page object
* @param {string} vaultAddr address of the vault server
* @param {string} clientToken vault token to connect to boundary
* @returns Name of the credential store
*/
exports.createVaultCredentialStore = async (page, vaultAddr, clientToken) => {
const credentialStoreName = 'Credential Store ' + nanoid();
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('link', { name: 'Credential Stores' })
.click();
await page.getByRole('link', { name: 'New', exact: true }).click();
await page.getByLabel('Name', { exact: true }).fill(credentialStoreName);
await page.getByLabel('Description').fill('This is an automated test');
await page.getByRole('group', { name: 'Type' }).getByLabel('Vault').click();
await page.getByLabel('Address', { exact: true }).fill(vaultAddr);
await page.getByLabel('Token', { exact: true }).fill(clientToken);
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByText(credentialStoreName),
).toBeVisible();
return credentialStoreName;
};
/** /**
* Uses the UI to create a static key pair credential . Assumes you have selected the desired project. * Uses the UI to create a static key pair credential . Assumes you have selected the desired project.
* @param {Page} page Playwright page object * @param {Page} page Playwright page object
* @param {string} username username for the credential
* @param {string} keyPath path to the private key
* @returns Name of the credential * @returns Name of the credential
*/ */
exports.createStaticCredentialKeyPair = async (page) => { exports.createStaticCredentialKeyPair = async (page, username, keyPath) => {
const credentialName = 'Credential ' + nanoid(); const credentialName = 'Credential ' + nanoid();
await page.getByRole('link', { name: 'Credentials', exact: true }).click(); await page.getByRole('link', { name: 'Credentials', exact: true }).click();
await page.getByRole('link', { name: 'New', exact: true }).click(); await page.getByRole('link', { name: 'New', exact: true }).click();
@@ -181,10 +217,8 @@ exports.createStaticCredentialKeyPair = async (page) => {
.getByRole('group', { name: 'Type' }) .getByRole('group', { name: 'Type' })
.getByLabel('Username & Key Pair') .getByLabel('Username & Key Pair')
.click(); .click();
await page await page.getByLabel('Username', { exact: true }).fill(username);
.getByLabel('Username', { exact: true }) const keyData = await readFile(keyPath, {
.fill(process.env.E2E_SSH_USER);
const keyData = await readFile(process.env.E2E_SSH_KEY_PATH, {
encoding: 'utf-8', encoding: 'utf-8',
}); });
await page.getByLabel('SSH Private Key', { exact: true }).fill(keyData); await page.getByLabel('SSH Private Key', { exact: true }).fill(keyData);
@@ -202,12 +236,107 @@ exports.createStaticCredentialKeyPair = async (page) => {
return credentialName; return credentialName;
}; };
/**
* Uses the UI to create a vault-generic credential library. Assumes you have selected
* the desired credential store.
* @param {Page} page Playwright page object
* @param {string} vaultPath path to secret in vault
* @param {string} credentialType type of credential for credential injection
* @returns Name of the credential library
*/
exports.createVaultGenericCredentialLibrary = async (
page,
vaultPath,
credentialType,
) => {
const credentialLibraryName = 'Credential Library ' + nanoid();
await page.getByRole('link', { name: 'Credential Libraries' }).click();
await page.getByRole('link', { name: 'New', exact: true }).click();
await page
.getByLabel('Name (Optional)', { exact: true })
.fill(credentialLibraryName);
await page
.getByLabel('Description (Optional)')
.fill('This is an automated test');
await page
.getByRole('group', { name: 'Type' })
.getByLabel('Generic Secrets')
.click();
await page.getByLabel('Vault Path').fill(vaultPath);
await page
.getByRole('combobox', { name: 'Credential Type' })
.selectOption(credentialType);
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
return credentialLibraryName;
};
/**
* Uses the UI to create a vault-ssh-certificate credential library. Assumes you have selected
* the desired credential store.
* @param {Page} page Playwright page object
* @param {string} vaultPath path to secret in vault
* @param {string} username username for the credential
* @returns Name of the credential library
*/
exports.createVaultSshCertificateCredentialLibrary = async (
page,
vaultPath,
username,
) => {
const credentialLibraryName = 'Credential Library ' + nanoid();
await page.getByRole('link', { name: 'Credential Libraries' }).click();
await page.getByRole('link', { name: 'New', exact: true }).click();
// Temporarily putting the Group selection first due to a bug where
// Name and Description fields get cleared when the Group is selected
await page
.getByRole('group', { name: 'Type' })
.getByLabel('SSH Certificates')
.click();
await page
.getByLabel('Name (Optional)', { exact: true })
.fill(credentialLibraryName);
await page
.getByLabel('Description (Optional)')
.fill('This is an automated test');
await page.getByLabel('Vault Path').fill(vaultPath);
await page.getByLabel('Username').fill(username);
await page.getByLabel('Key Type').selectOption('ecdsa');
await page.getByLabel('Key Bits').fill('521');
await page
.getByRole('group', { name: 'Extensions' })
.getByLabel('Key')
.fill('permity-pty');
await page
.getByRole('group', { name: 'Extensions' })
.getByRole('button', { name: 'Add' })
.click();
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
return credentialLibraryName;
};
/** /**
* Uses the UI to create a new target. Assumes you have selected the desired project. * Uses the UI to create a new target. Assumes you have selected the desired project.
* @param {Page} page Playwright page object * @param {Page} page Playwright page object
* @param {string} port Port of the target
* @returns Name of the target * @returns Name of the target
*/ */
exports.createNewTarget = async (page) => { exports.createNewTarget = async (page, port) => {
const targetName = 'Target ' + nanoid(); const targetName = 'Target ' + nanoid();
await page await page
.getByRole('navigation', { name: 'Resources' }) .getByRole('navigation', { name: 'Resources' })
@@ -216,7 +345,7 @@ exports.createNewTarget = async (page) => {
await page.getByRole('link', { name: 'New', exact: true }).click(); await page.getByRole('link', { name: 'New', exact: true }).click();
await page.getByLabel('Name').fill(targetName); await page.getByLabel('Name').fill(targetName);
await page.getByLabel('Description').fill('This is an automated test'); await page.getByLabel('Description').fill('This is an automated test');
await page.getByLabel('Default Port').fill(process.env.E2E_TARGET_PORT); await page.getByLabel('Default Port').fill(port);
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await expect( await expect(
page.getByRole('alert').getByText('Success', { exact: true }), page.getByRole('alert').getByText('Success', { exact: true }),
@@ -232,9 +361,11 @@ exports.createNewTarget = async (page) => {
/** /**
* Uses the UI to create a new target with address. Assumes you have selected the desired project. * Uses the UI to create a new target with address. Assumes you have selected the desired project.
* @param {Page} page Playwright page object * @param {Page} page Playwright page object
* @param {string} address Address of the target
* @param {string} port Port of the target
* @returns Name of the target * @returns Name of the target
*/ */
exports.createNewTargetWithAddress = async (page) => { exports.createNewTargetWithAddress = async (page, address, port) => {
const targetName = 'Target ' + nanoid(); const targetName = 'Target ' + nanoid();
await page await page
.getByRole('navigation', { name: 'Resources' }) .getByRole('navigation', { name: 'Resources' })
@@ -243,8 +374,8 @@ exports.createNewTargetWithAddress = async (page) => {
await page.getByRole('link', { name: 'New', exact: true }).click(); await page.getByRole('link', { name: 'New', exact: true }).click();
await page.getByLabel('Name').fill(targetName); await page.getByLabel('Name').fill(targetName);
await page.getByLabel('Description').fill('This is an automated test'); await page.getByLabel('Description').fill('This is an automated test');
await page.getByLabel('Target Address').fill(process.env.E2E_TARGET_ADDRESS); await page.getByLabel('Target Address').fill(address);
await page.getByLabel('Default Port').fill(process.env.E2E_TARGET_PORT); await page.getByLabel('Default Port').fill(port);
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await expect( await expect(
page.getByRole('alert').getByText('Success', { exact: true }), page.getByRole('alert').getByText('Success', { exact: true }),
@@ -261,9 +392,11 @@ exports.createNewTargetWithAddress = async (page) => {
* Uses the UI to create a new TCP target with address in boundary-enterprise * Uses the UI to create a new TCP target with address in boundary-enterprise
* Assumes you have selected the desired project. * Assumes you have selected the desired project.
* @param {Page} page Playwright page object * @param {Page} page Playwright page object
* @param {string} address Address of the target
* @param {string} port Port of the target
* @returns Name of the target * @returns Name of the target
*/ */
exports.createTcpTargetWithAddressEnt = async (page) => { exports.createTcpTargetWithAddressEnt = async (page, address, port) => {
const targetName = 'Target ' + nanoid(); const targetName = 'Target ' + nanoid();
await page await page
.getByRole('navigation', { name: 'Resources' }) .getByRole('navigation', { name: 'Resources' })
@@ -273,8 +406,8 @@ exports.createTcpTargetWithAddressEnt = async (page) => {
await page.getByLabel('Name').fill(targetName); await page.getByLabel('Name').fill(targetName);
await page.getByLabel('Description').fill('This is an automated test'); await page.getByLabel('Description').fill('This is an automated test');
await page.getByRole('group', { name: 'Type' }).getByLabel('TCP').click(); await page.getByRole('group', { name: 'Type' }).getByLabel('TCP').click();
await page.getByLabel('Target Address').fill(process.env.E2E_TARGET_ADDRESS); await page.getByLabel('Target Address').fill(address);
await page.getByLabel('Default Port').fill(process.env.E2E_TARGET_PORT); await page.getByLabel('Default Port').fill(port);
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await expect( await expect(
page.getByRole('alert').getByText('Success', { exact: true }), page.getByRole('alert').getByText('Success', { exact: true }),
@@ -291,9 +424,11 @@ exports.createTcpTargetWithAddressEnt = async (page) => {
* Uses the UI to create a new SSH target with address in boundary-enterprise * Uses the UI to create a new SSH target with address in boundary-enterprise
* Assumes you have selected the desired project. * Assumes you have selected the desired project.
* @param {Page} page Playwright page object * @param {Page} page Playwright page object
* @param {string} address Address of the target
* @param {string} port Port of the target
* @returns Name of the target * @returns Name of the target
*/ */
exports.createSshTargetWithAddressEnt = async (page) => { exports.createSshTargetWithAddressEnt = async (page, address, port) => {
const targetName = 'Target ' + nanoid(); const targetName = 'Target ' + nanoid();
await page await page
.getByRole('navigation', { name: 'Resources' }) .getByRole('navigation', { name: 'Resources' })
@@ -303,8 +438,48 @@ exports.createSshTargetWithAddressEnt = async (page) => {
await page.getByLabel('Name').fill(targetName); await page.getByLabel('Name').fill(targetName);
await page.getByLabel('Description').fill('This is an automated test'); await page.getByLabel('Description').fill('This is an automated test');
await page.getByRole('group', { name: 'Type' }).getByLabel('SSH').click(); await page.getByRole('group', { name: 'Type' }).getByLabel('SSH').click();
await page.getByLabel('Target Address').fill(process.env.E2E_TARGET_ADDRESS); await page.getByLabel('Target Address').fill(address);
await page.getByLabel('Default Port').fill(process.env.E2E_TARGET_PORT); await page.getByLabel('Default Port').fill(port);
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(
page.getByRole('navigation', { name: 'breadcrumbs' }).getByText(targetName),
).toBeVisible();
return targetName;
};
/**
* Uses the UI to create a new SSH target with address in boundary-enterprise
* Assumes you have selected the desired project.
* @param {Page} page Playwright page object
* @param {string} address Address of the target
* @param {string} port Port of the target
* @param {string} workerFilterEgress Egress worker filter
* @returns Name of the target
*/
exports.createSshTargetWithAddressAndWorkerFilterEnt = async (
page,
address,
port,
workerFilterEgress,
) => {
const targetName = 'Target ' + nanoid();
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('link', { name: 'Targets' })
.click();
await page.getByRole('link', { name: 'New', exact: true }).click();
await page.getByLabel('Name').fill(targetName);
await page.getByLabel('Description').fill('This is an automated test');
await page.getByRole('group', { name: 'Type' }).getByLabel('SSH').click();
await page.getByLabel('Target Address').fill(address);
await page.getByLabel('Default Port').fill(port);
await page.getByLabel('Egress worker filter').click();
await page.getByRole('textbox', { name: 'Filter' }).fill(workerFilterEgress);
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
await expect( await expect(
page.getByRole('alert').getByText('Success', { exact: true }), page.getByRole('alert').getByText('Success', { exact: true }),
@@ -372,7 +547,8 @@ exports.addHostSourceToTarget = async (page, hostSourceName) => {
}; };
/** /**
* Uses the UI to navigate to Sessions and waits for the session to appear. * Uses the UI to navigate to Sessions and waits for the session to appear
* and be in Active state.
* @param {Page} page Playwright page object * @param {Page} page Playwright page object
* @param {string} targetName Name of the target associated with the session * @param {string} targetName Name of the target associated with the session
*/ */
@@ -383,12 +559,16 @@ exports.waitForSessionToBeVisible = async (page, targetName) => {
.click(); .click();
let i = 0; let i = 0;
let sessionIsVisible = false; let sessionIsVisible = false;
let sessionIsActive = false;
do { do {
i = i + 1; i = i + 1;
sessionIsVisible = await page sessionIsVisible = await page
.getByRole('cell', { name: targetName }) .getByRole('cell', { name: targetName })
.isVisible(); .isVisible();
if (sessionIsVisible) { sessionIsActive = await page
.getByRole('cell', { name: 'Active' })
.isVisible();
if (sessionIsVisible && sessionIsActive) {
break; break;
} }
await page.getByRole('button', { name: 'Refresh' }).click(); await page.getByRole('button', { name: 'Refresh' }).click();
@@ -668,9 +848,10 @@ exports.addMemberToGroup = async (page, userName) => {
/** /**
* Uses the UI to create a new role. Assumes you have selected the desired scope. * Uses the UI to create a new role. Assumes you have selected the desired scope.
* @param {Page} page Playwright page object * @param {Page} page Playwright page object
* @param {string} roleName Name of the new role
*/ */
exports.createNewRole = async (page, roleName) => { exports.createRole = async (page) => {
const roleName = 'Role ' + nanoid();
await page await page
.getByRole('navigation', { name: 'IAM' }) .getByRole('navigation', { name: 'IAM' })
.getByRole('link', { name: 'Roles' }) .getByRole('link', { name: 'Roles' })
@@ -685,6 +866,8 @@ exports.createNewRole = async (page, roleName) => {
await expect( await expect(
page.getByRole('navigation', { name: 'breadcrumbs' }).getByText(roleName), page.getByRole('navigation', { name: 'breadcrumbs' }).getByText(roleName),
).toBeVisible(); ).toBeVisible();
return roleName;
}; };
/** /**
@@ -729,3 +912,116 @@ exports.addGrantsToGroup = async (page, grants) => {
page.getByRole('textbox', { name: 'Grant', exact: true }), page.getByRole('textbox', { name: 'Grant', exact: true }),
).toHaveValue(grants); ).toHaveValue(grants);
}; };
/**
* Uses the UI to create a new Storage Bucket. Assumes you have selected the desired scope.
* @param {Page} page Playwright page object
* @param {string} bucketName Name of the Storage Bucket
* @param {string} region Region of the Storage Bucket
* @param {string} accessKeyId Access Key ID for the Storage Bucket
* @param {string} secretAccessKey Secret Access Key for the Storage Bucket
* @param {string} workerFilter Worker filter for the Storage Bucket
* @returns Name of the Storage Bucket
*/
exports.createStorageBucket = async (
page,
bucketName,
region,
accessKeyId,
secretAccessKey,
workerFilter,
) => {
const storageBucketName = 'Bucket ' + nanoid();
await page
.getByRole('link', { name: 'Storage Buckets', exact: true })
.click();
await page.getByRole('link', { name: 'New Storage Bucket' }).click();
await page.getByLabel(new RegExp('Name*')).fill(storageBucketName);
await page.getByLabel('Bucket name').fill(bucketName);
await page.getByLabel('Region').fill(region);
await page.getByLabel('Access key ID').fill(accessKeyId);
await page.getByLabel('Secret access key').fill(secretAccessKey);
await page.getByLabel('Worker filter').fill(workerFilter);
await page.getByLabel('Disable credential rotation').click();
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByText(storageBucketName),
).toBeVisible();
return storageBucketName;
};
/**
* Uses the UI to enable session recording for a target. Assumes you have selected the desired target.
* @param {Page} page Playwright page object
* @param {string} storageBucketName name of the Storage Bucket used for session recording
*/
exports.enableSessionRecording = async (page, storageBucketName) => {
await page.getByText('Enable recording').click();
await page.getByLabel('Record sessions for this target').click();
await page
.getByLabel('AWS storage buckets')
.selectOption({ label: storageBucketName });
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(
page.getByRole('listitem').getByText(storageBucketName),
).toBeVisible();
};
/**
* Uses the UI to create a Storage Policy. Assumes you have selected the desired scope.
* @param {Page} page Playwright page object
* @returns Name of the Storage Policy
*/
exports.createStoragePolicy = async (page) => {
const storagePolicyName = 'Policy ' + nanoid();
await page
.getByRole('link', { name: 'Storage Policies', exact: true })
.click();
await page
.getByRole('link', { name: 'Create a new storage policy', exact: true })
.click();
await page.getByLabel('Name').fill(storagePolicyName);
await page.getByLabel('Retention Policy').selectOption({ label: 'Forever' });
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByText(storagePolicyName),
).toBeVisible();
return storagePolicyName;
};
/**
* Uses the UI to attach Storage Policy to a scope. Assumes you have selected the desired scope.
* @param {Page} page Playwright page object
* @param {string} policyName name of the Policy to be attached to the scope
*/
exports.attachStoragePolicy = async (page, policyName) => {
await page
.getByRole('link', { name: new RegExp('/*Settings'), exact: true })
.click();
await page.getByText('Add Storage Policy').click();
await page.getByLabel('Storage Policy').selectOption({ label: policyName });
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await expect(page.getByRole('listitem').getByText(policyName)).toBeVisible();
};
Loaded 100 of 142 files, more files were not shown because too many files have changed in this diff. Show more