Add injected credentials tab (#1295)
* chore: 🤖 introduce injected application credentials on the UI
This commit is contained in:
1 parent
128b0ff492
commit
8952541203
15 files changed
+520
-14
No files matched your search
@@ -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;
|
||||
}
|
||||
+46
@@ -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>
|
||||
+28
@@ -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>
|
||||
|
||||
|
||||
+6
-8
@@ -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];
|
||||
}
|
||||
|
||||
+49
@@ -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;
|
||||
|
||||
+20
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
+83
@@ -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
|
||||
|
||||
+58
@@ -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();
|
||||
}
|
||||
}
|
||||
+54
@@ -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
|
||||
|
||||
+142
@@ -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>
|
||||
Reference in new issue
Block a user