Add injected credentials tab (#1295)

* chore: 🤖 introduce injected application credentials on the UI
This commit is contained in:
Dharini Jeeva authored and GitHub committed 2022-09-02 12:29:39 -07:00
1 parent 128b0ff492
commit 8952541203
15 files changed
+520 -14

No files matched your search

+8 -1
View File
@@ -1,3 +1,10 @@
import GeneratedCredentialModel from '../generated/models/credential';
import { equal } from '@ember/object/computed';
export default class CredentialModel extends GeneratedCredentialModel {}
export default class CredentialModel extends GeneratedCredentialModel {
// =attributes
/**
* @type {boolean}
*/
@equal('type', 'username_password') isUsernamePassword;
}
@@ -0,0 +1,46 @@
<Rose::Form
class='full-width'
@onSubmit={{fn @submit this.selectedCredentialSourceIDs}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
as |form|
>
<form.actions
@submitText={{t
'resources.target.actions.add-injected-application-credential-sources'
}}
@cancelText={{t 'actions.cancel'}}
/>
<Rose::Table as |table|>
<table.header as |header|>
<header.row as |row|>
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
{{#each @filteredCredentialSources as |credentialSource|}}
<body.row as |row|>
<row.headerCell>
<form.checkbox
@label={{credentialSource.id}}
@description={{credentialSource.description}}
@onChange={{fn this.toggleCredentialSource credentialSource.id}}
/>
</row.headerCell>
<row.cell>{{credentialSource.name}}</row.cell>
<row.cell>
<Rose::Badge>{{t
(concat
'resources.target.injected-application-credential-source.types.'
credentialSource.type
)
}}</Rose::Badge>
</row.cell>
</body.row>
{{/each}}
</table.body>
</Rose::Table>
</Rose::Form>
@@ -0,0 +1,28 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { A } from '@ember/array';
export default class FormTargetAddInjectedApplicationCredentialSourcesIndexComponent extends Component {
// =properties
/**
* Array of selected credential source IDs.
* @type {EmberArray}
*/
selectedCredentialSourceIDs = A();
// =actions
/**
* Add/Remove credential source to current selection
* @param {string} credentialSourceId
*/
@action
toggleCredentialSource(credentialSourceId) {
if (!this.selectedCredentialSourceIDs.includes(credentialSourceId)) {
this.selectedCredentialSourceIDs.addObject(credentialSourceId);
} else {
this.selectedCredentialSourceIDs.removeObject(credentialSourceId);
}
}
}
@@ -15,6 +15,16 @@
{{t 'resources.target.actions.add-brokered-credential-sources'}}
</dropdown.link>
{{/if}}
{{#if (can 'addInjectedApplicationCredentialSources target' @model)}}
<dropdown.link
@route='scopes.scope.targets.target.add-injected-application-credential-sources'
>
{{t
'resources.target.actions.add-injected-application-credential-sources'
}}
</dropdown.link>
{{/if}}
{{#if (can 'delete model' @model)}}
<dropdown.separator />
<dropdown.button
@@ -8,4 +8,15 @@
<nav.link @route='scopes.scope.targets.target.brokered-credential-sources'>
{{t 'resources.target.brokered-credential-source.title_plural'}}
</nav.link>
{{#if @model.isSSH}}
<nav.link
@route='scopes.scope.targets.target.injected-application-credential-sources'
>
{{t
'resources.target.injected-application-credential-source.title_plural'
}}
</nav.link>
{{/if}}
</Rose::Nav::Tabs>
@@ -1,6 +1,5 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
export default class ScopesScopeTargetsTargetAddBrokeredCredentialSourcesController extends Controller {
// =services
@@ -21,20 +20,19 @@ export default class ScopesScopeTargetsTargetAddBrokeredCredentialSourcesControl
* Filter out credential sources not already added to the target.
* @type {[CredentialLibraryModel, CredentialModel]}
*/
@computed(
'model.{target.brokered_credential_source_ids.[],credentialLibraries.[],credentials.[]}'
)
get filteredCredentialSources() {
// Get IDs for credential sources already added to the current target
const currentCredentialSourceIDs =
const currentCredentialSourceIDs = new Set(
this.model.target.brokered_credential_source_ids.map(
(source) => source.value
);
)
);
const notAddedCredentialLibraries = this.model.credentialLibraries.filter(
({ id }) => !currentCredentialSourceIDs.includes(id)
({ id }) => !currentCredentialSourceIDs.has(id)
);
const notAddedCredentials = this.model.credentials.filter(
({ id }) => !currentCredentialSourceIDs.includes(id)
({ id }) => !currentCredentialSourceIDs.has(id)
);
return [...notAddedCredentialLibraries, ...notAddedCredentials];
}
@@ -0,0 +1,49 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ScopesScopeTargetsTargetAddInjectedApplicationCredentialSourcesController extends Controller {
// =services
@service intl;
// =attributes
/**
* Checks for unassigned credential sources.
* @type {boolean}
*/
get hasAvailableInjectedApplicationCredentialSources() {
return this.filteredCredentialSources.length > 0;
}
/**
* Filter out credential sources not already added to the target.
* @type {[CredentialLibraryModel, CredentialModel]}
*/
get filteredCredentialSources() {
// Get IDs for credential sources already added to the current target
const currentCredentialSourceIDs = new Set(
this.model.target.injected_application_credential_source_ids.map(
(source) => source.value
)
);
const notAddedCredentialLibraries = this.model.credentialLibraries.filter(
({ id }) => !currentCredentialSourceIDs.has(id)
);
const notAddedCredentials = this.model.credentials.filter(
({ id }) => !currentCredentialSourceIDs.has(id)
);
return [...notAddedCredentialLibraries, ...notAddedCredentials];
}
/**
* Translated roles breadcrumb
* @type {string}
*/
get breadCrumb() {
return this.intl.t(
'resources.target.actions.add-injected-application-credential-sources'
);
}
}
@@ -1,7 +1,7 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ScopesScopeTargetsTargetCredentialSourcesController extends Controller {
export default class ScopesScopeTargetsTargetBrokeredCredentialSourcesController extends Controller {
// =services
@service intl;
@@ -0,0 +1,20 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ScopesScopeTargetsTargetInjectedApplicationCredentialSourcesController extends Controller {
// =services
@service intl;
// =attributes
/**
* Translated credential library breadcrumb
* @type {string}
*/
get breadCrumb() {
return this.intl.t(
'resources.target.injected-application-credential-source.title_plural'
);
}
}
@@ -0,0 +1,83 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { all } from 'rsvp';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeTargetsTargetAddInjectedApplicationCredentialSourcesRoute extends Route {
// =services
@service intl;
@service router;
// =methods
/**
* Empty out any previously loaded credential sources.
*/
beforeModel() {
this.store.unloadAll('credential-library');
this.store.unloadAll('credential');
}
/**
* Returns the current target and credential sources.
* @return {{target: TargetModel, credentialLibraries: [CredentialLibraryModel], credentials: [CredentialModel]}}
*/
async model() {
const target = this.modelFor('scopes.scope.targets.target');
const { id: scope_id } = this.modelFor('scopes.scope');
const credentialStores = await this.store.query('credential-store', {
scope_id,
});
await all(
credentialStores.map(({ id: credential_store_id, isStatic }) => {
if (isStatic) {
return this.store.query('credential', {
credential_store_id,
});
} else {
return this.store.query('credential-library', {
credential_store_id,
});
}
})
);
const credentialLibraries = this.store.peekAll('credential-library');
const credentials = this.store.peekAll('credential');
return {
target,
credentialLibraries,
credentials,
};
}
// =actions
/**
* Add credential libraries to current target
* @param {TargetModel} target
* @param {[string]} credentialLibraryIDs
*/
@action
@loading
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.add-success')
async save(target, credentialLibraryIDs) {
await target.addInjectedApplicationCredentialSources(credentialLibraryIDs);
this.router.replaceWith(
'scopes.scope.targets.target.injected-application-credential-sources'
);
}
/**
* Redirect to target credential sources as if nothing ever happened.
*/
@action
cancel() {
this.router.replaceWith(
'scopes.scope.targets.target.injected-application-credential-sources'
);
}
}
@@ -9,7 +9,7 @@ export default class ScopesScopeTargetsTargetBrokeredCredentialSourcesRoute exte
// =methods
/**
* Loads all credential libraries under the current target.
* Loads all credential sources under the current target.
* @return {Promise{[CredentialLibraryModel, CredentialModel]}}
*/
beforeModel() {
@@ -43,9 +43,9 @@ export default class ScopesScopeTargetsTargetBrokeredCredentialSourcesRoute exte
// =actions
/**
* Remove a credential library from the current target.
* Remove a credential source from the current target.
* @param {TargetModel} target
* @param {CredentialLibraryModel} credentialLibrary
* @param {CredentialLibraryModel, credentialModel} credentialSource
*/
@action
@loading
@@ -0,0 +1,58 @@
import Route from '@ember/routing/route';
import { all } from 'rsvp';
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 ScopesScopeTargetsTargetInjectedApplicationCredentialSourcesRoute extends Route {
// =methods
/**
* Loads all credential sources under the current target.
* @return {Promise{[CredentialLibraryModel, CredentialModel]}}
*/
beforeModel() {
const { injected_application_credential_source_ids: sourceIDFragments } =
this.modelFor('scopes.scope.targets.target');
return all(
sourceIDFragments.map(({ value }) => {
const isStatic = value.includes('cred');
if (isStatic) {
return this.store.findRecord('credential', value, {
reload: true,
});
} else {
return this.store.findRecord('credential-library', value, {
reload: true,
});
}
})
);
}
/**
* Returns the previously loaded target instance.
* @return {TargetModel}
*/
model() {
return this.modelFor('scopes.scope.targets.target');
}
// =actions
/**
* Remove a credential source from the current target.
* @param {TargetModel} target
* @param {CredentialLibraryModel, credentialModel} credentialSource
*/
@action
@loading
@confirm('questions.remove-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.remove-success')
async removeInjectedApplicationCredentialSource(target, credentialSource) {
await target.removeInjectedApplicationCredentialSource(credentialSource.id);
this.refresh();
}
}
@@ -0,0 +1,54 @@
{{page-title
(t 'resources.target.actions.add-injected-application-credential-sources')
}}
<Rose::Layout::Page as |page|>
<page.breadcrumbs>
<BreadCrumbs />
</page.breadcrumbs>
<page.header>
<h2>
{{t
'resources.target.injected-application-credential-source.messages.add.title'
}}
</h2>
<p>{{t
'resources.target.injected-application-credential-source.messages.add.description'
}}</p>
</page.header>
<page.body>
{{#if this.hasAvailableInjectedApplicationCredentialSources}}
<Form::Target::AddInjectedApplicationCredentialSources
@model={{@model.target}}
@filteredCredentialSources={{this.filteredCredentialSources}}
@submit={{route-action 'save' @model.target}}
@cancel={{route-action 'cancel'}}
/>
{{/if}}
{{#unless this.hasAvailableInjectedApplicationCredentialSources}}
<Rose::Layout::Centered>
<Rose::Message
@title={{t
'resources.target.injected-application-credential-source.messages.none.title'
}}
as |message|
>
<message.description>
{{t
'resources.target.injected-application-credential-source.messages.none.description'
}}
</message.description>
<message.link
@route='scopes.scope.targets.target.injected-application-credential-sources'
>
<Rose::Icon @name='flight-icons/svg/chevron-left-16' />
{{t 'actions.back'}}
</message.link>
</Rose::Message>
</Rose::Layout::Centered>
{{/unless}}
</page.body>
</Rose::Layout::Page>
@@ -37,7 +37,7 @@
}}
<body.row as |row|>
<row.headerCell>
{{#if (eq brokeredCredentialSource.type 'username_password')}}
{{#if brokeredCredentialSource.isUsernamePassword}}
<LinkTo
@route='scopes.scope.credential-stores.credential-store.credentials.credential'
@models={{array
@@ -0,0 +1,142 @@
{{page-title
(t 'resources.target.injected-application-credential-source.title_plural')
}}
<Rose::Layout::Page as |page|>
<page.breadcrumbs>
<BreadCrumbs />
</page.breadcrumbs>
<page.header>
<Targets::Target::Header @model={{@model}} />
</page.header>
<page.navigation>
<Targets::Target::Nav @model={{@model}} />
</page.navigation>
<page.actions>
<Targets::Target::Actions @model={{@model}} />
</page.actions>
<page.body>
{{#if @model.injectedApplicationCredentialSources}}
<Rose::Table as |table|>
<table.header as |header|>
<header.row as |row|>
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
<row.headerCell>{{t 'titles.actions'}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
{{#each
@model.injectedApplicationCredentialSources
as |injectedApplicationCredentialSource|
}}
<body.row as |row|>
<row.headerCell>
{{#if injectedApplicationCredentialSource.isUsernamePassword}}
<LinkTo
@route='scopes.scope.credential-stores.credential-store.credentials.credential'
@models={{array
injectedApplicationCredentialSource.credential_store_id
injectedApplicationCredentialSource.id
}}
>
{{injectedApplicationCredentialSource.displayName}}
</LinkTo>
{{else}}
<LinkTo
@route='scopes.scope.credential-stores.credential-store.credential-libraries.credential-library'
@models={{array
injectedApplicationCredentialSource.credential_store_id
injectedApplicationCredentialSource.id
}}
>
{{injectedApplicationCredentialSource.displayName}}
</LinkTo>
{{/if}}
<p>{{injectedApplicationCredentialSource.description}}</p>
</row.headerCell>
<row.cell>
<Rose::Badge>{{t
(concat
'resources.target.injected-application-credential-source.types.'
injectedApplicationCredentialSource.type
)
}}</Rose::Badge>
</row.cell>
<row.cell>
<Copyable
@text={{injectedApplicationCredentialSource.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<code>{{injectedApplicationCredentialSource.id}}</code>
</Copyable>
</row.cell>
<row.cell>
{{#if (can 'removeCredentialSources target' @model)}}
<Rose::Dropdown
@icon='flight-icons/svg/more-horizontal-16'
@iconOnly={{true}}
@showCaret={{false}}
@dropdownRight={{true}}
@text={{t 'actions.manage'}}
as |dropdown|
>
<dropdown.button
@style='danger'
{{on
'click'
(route-action
'removeInjectedApplicationCredentialSource'
@model
injectedApplicationCredentialSource
)
}}
>
{{t 'actions.remove'}}
</dropdown.button>
</Rose::Dropdown>
{{/if}}
</row.cell>
</body.row>
{{/each}}
</table.body>
</Rose::Table>
{{else}}
<Rose::Layout::Centered>
<Rose::Message
@title={{t
'resources.target.injected-application-credential-source.messages.welcome.title'
}}
as |message|
>
<message.description>
{{t
'resources.target.injected-application-credential-source.messages.welcome.description'
}}
</message.description>
<message.link
@route='scopes.scope.targets.target.add-injected-application-credential-sources'
>
<Rose::Icon @name='flight-icons/svg/plus-circle-16' />
{{t
'resources.target.actions.add-injected-application-credential-sources'
}}
</message.link>
</Rose::Message>
</Rose::Layout::Centered>
{{/if}}
</page.body>
</Rose::Layout::Page>