mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-08 16:24:51 -04:00
[UI] Ember Data Migration - SSH Role Sign Key and Generate Credential Views | Vault-45234 (#15100) (#15107)
* migrated ssh views - list, detail, create and edit * adds validation for role name and update test attributes for consistency * updated sign key attr name in test * migrated ssh views - list, detail, create and edit * adds validation for role name and update test attributes for consistency * updated sign key attr name in test * moved flat ordering logic to form as per dynamic selection * Humanized TTL field display value * Apply suggestions from code review * fixed prettier issue * VAULT-45234 - Migrates SSH credential generation and signing components with forms and Api service * fixed review comments * Apply suggestions from code review --------- Co-authored-by: mohit-hashicorp <mohit.ojha@hashicorp.com> Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
This commit is contained in:
parent
65e0793e46
commit
5a7eb39077
11 changed files with 433 additions and 151 deletions
77
ui/app/components/generate-credentials-ssh.hbs
Normal file
77
ui/app/components/generate-credentials-ssh.hbs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2026
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Page::Header @title="Generate SSH Credentials">
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
{{#if this.otpData}}
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
<Hds::Alert @type="inline" @color="warning" class="has-top-bottom-margin" data-test-warning as |A|>
|
||||
<A.Title>Warning</A.Title>
|
||||
<A.Description>
|
||||
You will not be able to access this information later, so please copy the information below.
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
{{#each this.otpDisplayRows as |row|}}
|
||||
{{#if row.masked}}
|
||||
<InfoTableRow @label={{row.label}} @value={{row.value}}>
|
||||
<MaskedInput @value={{row.value}} @name="key" @displayOnly={{true}} @allowCopy={{true}} />
|
||||
</InfoTableRow>
|
||||
{{else}}
|
||||
<InfoTableRow @label={{row.label}} @value={{row.value}} />
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<Hds::Copy::Button
|
||||
@text="Copy credentials"
|
||||
@textToCopy={{this.otpData.key}}
|
||||
@onError={{(fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger"))}}
|
||||
class="primary"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<Hds::Button @text="Back" @color="secondary" {{on "click" this.reset}} data-test-back-button />
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<form {{on "submit" (perform this.generate)}} data-test-secret-generate-form>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="generate" @noun="credential" />
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
{{#each this.credentialForm.formFields as |attr|}}
|
||||
<FormField
|
||||
data-test-field
|
||||
@attr={{attr}}
|
||||
@model={{this.credentialForm}}
|
||||
@modelValidations={{this.modelValidations}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{#if this.invalidFormAlert}}
|
||||
<AlertInline @type="danger" @message={{this.invalidFormAlert}} class="has-top-padding-s" />
|
||||
{{/if}}
|
||||
</div>
|
||||
<Hds::ButtonSet class="has-top-bottom-margin">
|
||||
<Hds::Button
|
||||
@text="Generate"
|
||||
@icon={{if this.generate.isRunning "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.generate.isRunning}}
|
||||
data-test-submit
|
||||
/>
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@route="vault.cluster.secrets.backend.list-root"
|
||||
@color="secondary"
|
||||
@model={{@backendPath}}
|
||||
data-test-cancel
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</form>
|
||||
{{/if}}
|
||||
90
ui/app/components/generate-credentials-ssh.ts
Normal file
90
ui/app/components/generate-credentials-ssh.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import SshOtpCredentialForm from 'vault/forms/ssh/otp-credential';
|
||||
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type ControlGroupService from 'vault/vault/services/control-group';
|
||||
|
||||
interface Args {
|
||||
backendPath: string;
|
||||
roleName: string;
|
||||
}
|
||||
|
||||
export default class GenerateCredentialsSsh extends Component<Args> {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly controlGroup: ControlGroupService;
|
||||
|
||||
@tracked credentialForm = new SshOtpCredentialForm();
|
||||
@tracked otpData: Record<string, unknown> | null = null;
|
||||
@tracked errorMessage: string | null = null;
|
||||
@tracked modelValidations: Record<string, unknown> | null = null;
|
||||
@tracked invalidFormAlert: string | null = null;
|
||||
|
||||
get otpDisplayRows() {
|
||||
const data = this.otpData;
|
||||
if (!data) return [];
|
||||
return [
|
||||
{ label: 'Username', value: data['username'] },
|
||||
{ label: 'IP Address', value: data['ip'] },
|
||||
{ label: 'Key', value: data['key'], masked: true },
|
||||
{ label: 'Key type', value: data['key_type'] },
|
||||
{ label: 'Port', value: data['port'] },
|
||||
].filter((f) => f.value != null && f.value !== '');
|
||||
}
|
||||
|
||||
get breadcrumbs() {
|
||||
const { backendPath, roleName } = this.args;
|
||||
return [
|
||||
{ label: backendPath, route: 'vault.cluster.secrets.backend', model: backendPath },
|
||||
{ label: 'Credentials', route: 'vault.cluster.secrets.backend', model: backendPath },
|
||||
{ label: roleName, route: 'vault.cluster.secrets.backend.show', model: roleName },
|
||||
{ label: 'Generate SSH credentials' },
|
||||
];
|
||||
}
|
||||
|
||||
generate = task(
|
||||
waitFor(async (evt: Event) => {
|
||||
evt.preventDefault();
|
||||
this.errorMessage = null;
|
||||
|
||||
const { isValid, state, invalidFormMessage, data } = this.credentialForm.toJSON();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = isValid ? null : invalidFormMessage;
|
||||
if (!isValid) return;
|
||||
try {
|
||||
const result = await this.api.secrets.sshGenerateCredentials(
|
||||
this.args.roleName,
|
||||
this.args.backendPath,
|
||||
data
|
||||
);
|
||||
this.otpData = (result.data as Record<string, unknown>) ?? {};
|
||||
} catch (error) {
|
||||
const { message, response } = await this.api.parseError(error);
|
||||
if (response?.isControlGroupError) {
|
||||
this.controlGroup.saveTokenFromError(response);
|
||||
this.errorMessage = this.controlGroup.logFromError(response).content;
|
||||
} else {
|
||||
this.errorMessage = message;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.otpData = null;
|
||||
this.credentialForm = new SshOtpCredentialForm();
|
||||
this.errorMessage = null;
|
||||
this.modelValidations = null;
|
||||
this.invalidFormAlert = null;
|
||||
}
|
||||
}
|
||||
81
ui/app/components/ssh-sign-key.hbs
Normal file
81
ui/app/components/ssh-sign-key.hbs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2026
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Page::Header @title="Sign SSH key">
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
{{#if this.signedKeyData}}
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
<Hds::Alert @type="inline" @color="warning" class="has-top-margin-s has-bottom-margin-s" as |A|>
|
||||
<A.Title>Warning</A.Title>
|
||||
<A.Description>
|
||||
You will not be able to access this information later, so please copy the information below.
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
{{#each this.signDisplayRows as |row|}}
|
||||
<InfoTableRow @label={{row.label}} @value={{row.value}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<Hds::Copy::Button
|
||||
@text="Copy key"
|
||||
@textToCopy={{this.signedKeyData.signed_key}}
|
||||
@onError={{(fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger"))}}
|
||||
class="primary"
|
||||
/>
|
||||
</div>
|
||||
{{#if this.signedKeyData.lease_id}}
|
||||
<div class="control">
|
||||
<Hds::Copy::Button
|
||||
@text="Copy lease ID"
|
||||
@textToCopy={{this.signedKeyData.lease_id}}
|
||||
@onError={{(fn
|
||||
(set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger")
|
||||
)}}
|
||||
class="secondary"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="control">
|
||||
<Hds::Button @text="Back" @color="secondary" {{on "click" this.reset}} data-test-back-button />
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<form {{on "submit" (perform this.sign)}} data-test-secret-generate-form="true">
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
<NamespaceReminder @mode="sign" @noun="SSH key" />
|
||||
<FormFieldGroups
|
||||
@model={{this.signForm}}
|
||||
@mode="create"
|
||||
@groupName="formFieldGroups"
|
||||
@modelValidations={{this.modelValidations}}
|
||||
/>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<AlertInline @type="danger" @message={{this.invalidFormAlert}} class="has-top-padding-s" />
|
||||
{{/if}}
|
||||
</div>
|
||||
<Hds::ButtonSet class="has-top-bottom-margin">
|
||||
<Hds::Button
|
||||
@text="Sign"
|
||||
@icon={{if this.sign.isRunning "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.sign.isRunning}}
|
||||
data-test-submit
|
||||
/>
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route="vault.cluster.secrets.backend.list-root"
|
||||
@model={{@backendPath}}
|
||||
data-test-cancel
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</form>
|
||||
{{/if}}
|
||||
90
ui/app/components/ssh-sign-key.ts
Normal file
90
ui/app/components/ssh-sign-key.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import SshSignForm from 'vault/forms/ssh/sign';
|
||||
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type ControlGroupService from 'vault/vault/services/control-group';
|
||||
|
||||
interface Args {
|
||||
roleName: string;
|
||||
backendPath: string;
|
||||
}
|
||||
|
||||
export default class SshSignKey extends Component<Args> {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly controlGroup: ControlGroupService;
|
||||
|
||||
@tracked signForm = new SshSignForm({ cert_type: 'user' });
|
||||
@tracked signedKeyData: Record<string, unknown> | null = null;
|
||||
@tracked errorMessage: string | null = null;
|
||||
@tracked modelValidations: Record<string, unknown> | null = null;
|
||||
@tracked invalidFormAlert: string | null = null;
|
||||
|
||||
get signDisplayRows() {
|
||||
const data = this.signedKeyData;
|
||||
if (!data) return [];
|
||||
return [
|
||||
{ label: 'Signed key', value: data['signed_key'] },
|
||||
{ label: 'Lease ID', value: data['lease_id'] },
|
||||
{ label: 'Renewable', value: data['renewable'] },
|
||||
{ label: 'Lease duration', value: data['lease_duration'] },
|
||||
{ label: 'Serial number', value: data['serial_number'] },
|
||||
].filter((f) => f.value != null && f.value !== '');
|
||||
}
|
||||
|
||||
get breadcrumbs() {
|
||||
const { backendPath, roleName } = this.args;
|
||||
return [
|
||||
{ label: backendPath, route: 'vault.cluster.secrets.backend', model: backendPath },
|
||||
{ label: 'Sign', route: 'vault.cluster.secrets.backend', model: backendPath },
|
||||
{ label: roleName, route: 'vault.cluster.secrets.backend.show', model: roleName },
|
||||
{ label: 'Sign SSH Key' },
|
||||
];
|
||||
}
|
||||
|
||||
sign = task(
|
||||
waitFor(async (evt: Event) => {
|
||||
evt.preventDefault();
|
||||
this.errorMessage = null;
|
||||
|
||||
const { isValid, state, invalidFormMessage, data } = this.signForm.toJSON();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = isValid ? null : invalidFormMessage;
|
||||
if (!isValid) return;
|
||||
try {
|
||||
const result = await this.api.secrets.sshSignCertificate(
|
||||
this.args.roleName,
|
||||
this.args.backendPath,
|
||||
data
|
||||
);
|
||||
this.signedKeyData = { ...result, ...(result.data as Record<string, unknown>) };
|
||||
} catch (error) {
|
||||
const { message, response } = await this.api.parseError(error);
|
||||
if (response?.isControlGroupError) {
|
||||
this.controlGroup.saveTokenFromError(response);
|
||||
this.errorMessage = this.controlGroup.logFromError(response).content;
|
||||
} else {
|
||||
this.errorMessage = message;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.signedKeyData = null;
|
||||
this.signForm = new SshSignForm({ cert_type: 'user' });
|
||||
this.errorMessage = null;
|
||||
this.modelValidations = null;
|
||||
this.invalidFormAlert = null;
|
||||
}
|
||||
}
|
||||
24
ui/app/forms/ssh/otp-credential.ts
Normal file
24
ui/app/forms/ssh/otp-credential.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Form from 'vault/forms/form';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
import { Validations } from 'vault/vault/app-types';
|
||||
|
||||
interface SshOtpCredentialData {
|
||||
username: string;
|
||||
ip: string;
|
||||
}
|
||||
|
||||
export default class SshOtpCredentialForm extends Form<SshOtpCredentialData> {
|
||||
formFields = [
|
||||
new FormField('username', 'string', { label: 'Username' }),
|
||||
new FormField('ip', 'string', { label: 'IP address' }),
|
||||
];
|
||||
|
||||
validations: Validations = {
|
||||
ip: [{ type: 'presence', message: 'IP address is required' }],
|
||||
};
|
||||
}
|
||||
49
ui/app/forms/ssh/sign.ts
Normal file
49
ui/app/forms/ssh/sign.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Form from 'vault/forms/form';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
import FormFieldGroup from 'vault/utils/forms/field-group';
|
||||
import { Validations } from 'vault/vault/app-types';
|
||||
|
||||
interface SshSignData {
|
||||
public_key: string;
|
||||
key_id?: string;
|
||||
valid_principals?: string;
|
||||
cert_type?: string;
|
||||
critical_options?: Record<string, unknown>;
|
||||
extensions?: Record<string, unknown>;
|
||||
ttl?: string;
|
||||
}
|
||||
|
||||
export default class SshSignForm extends Form<SshSignData> {
|
||||
get formFieldGroups() {
|
||||
return [
|
||||
new FormFieldGroup('default', [
|
||||
new FormField('public_key', 'string', { label: 'Public key' }),
|
||||
new FormField('valid_principals', 'string', {
|
||||
label: 'Valid principals',
|
||||
helpText:
|
||||
'Specifies valid principals, either usernames or hostnames, that the certificate should be signed for. Required unless the role has specified allow_empty_principals.',
|
||||
}),
|
||||
]),
|
||||
new FormFieldGroup('More options', [
|
||||
new FormField('key_id', 'string', { label: 'Key ID' }),
|
||||
new FormField('cert_type', 'string', {
|
||||
label: 'Certificate Type',
|
||||
possibleValues: ['user', 'host'],
|
||||
defaultValue: 'user',
|
||||
}),
|
||||
new FormField('critical_options', 'object', { label: 'Critical Options' }),
|
||||
new FormField('extensions', 'object', { label: 'Extensions' }),
|
||||
new FormField('ttl', 'string', { label: 'TTL', editType: 'ttl' }),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
validations: Validations = {
|
||||
public_key: [{ type: 'presence', message: 'Public Key is required' }],
|
||||
};
|
||||
}
|
||||
|
|
@ -20,10 +20,6 @@ export default Route.extend({
|
|||
if (!SUPPORTED_DYNAMIC_BACKENDS.includes(backendType)) {
|
||||
return this.router.transitionTo('vault.cluster.secrets.backend.list-root', backendPath);
|
||||
}
|
||||
// hydrate model if backend type is ssh
|
||||
if (backendType === 'ssh') {
|
||||
this.pathHelp.hydrateModel('ssh-otp-credential', backendPath);
|
||||
}
|
||||
|
||||
// assign back button route
|
||||
if (backendType === 'totp') {
|
||||
|
|
|
|||
|
|
@ -1,32 +1,21 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import UnloadModel from 'vault/mixins/unload-model-route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default Route.extend(UnloadModel, {
|
||||
export default Route.extend({
|
||||
router: service(),
|
||||
store: service(),
|
||||
capabilities: service(),
|
||||
templateName: 'vault/cluster/secrets/backend/sign',
|
||||
|
||||
backendModel() {
|
||||
return this.modelFor('vault.cluster.secrets.backend');
|
||||
},
|
||||
|
||||
pathQuery(role, backend) {
|
||||
return {
|
||||
id: `${backend}/sign/${role}`,
|
||||
};
|
||||
},
|
||||
|
||||
pathForType() {
|
||||
return 'sign';
|
||||
},
|
||||
|
||||
model(params) {
|
||||
async model(params) {
|
||||
const role = params.secret;
|
||||
const backendModel = this.backendModel();
|
||||
const backend = backendModel.id;
|
||||
|
|
@ -34,23 +23,16 @@ export default Route.extend(UnloadModel, {
|
|||
if (backendModel.type !== 'ssh') {
|
||||
return this.router.transitionTo('vault.cluster.secrets.backend.list-root', backend);
|
||||
}
|
||||
return this.store.queryRecord('capabilities', this.pathQuery(role, backend)).then((capabilities) => {
|
||||
if (!capabilities.canUpdate) {
|
||||
return this.router.transitionTo('vault.cluster.secrets.backend.list-root', backend);
|
||||
}
|
||||
return this.store.createRecord('ssh-sign', {
|
||||
role: {
|
||||
backend,
|
||||
id: role,
|
||||
name: role,
|
||||
},
|
||||
id: `${backend}-${role}`,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
controller.set('backend', this.backendModel());
|
||||
const signPath = this.capabilities.pathFor('sshSign', { backend, id: role });
|
||||
const capabilities = await this.capabilities.fetch([signPath]);
|
||||
if (!capabilities[signPath]?.canUpdate) {
|
||||
return this.router.transitionTo('vault.cluster.secrets.backend.list-root', backend);
|
||||
}
|
||||
|
||||
return {
|
||||
roleName: role,
|
||||
backendPath: backend,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
Copyright IBM Corp. 2016, 2026
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
|
|
@ -18,6 +18,8 @@
|
|||
@keyName={{this.model.keyName}}
|
||||
@totpCodePeriod={{this.model.totpCodePeriod}}
|
||||
/>
|
||||
{{else if (eq this.model.backendType "ssh")}}
|
||||
<GenerateCredentialsSsh @backendPath={{this.model.backendPath}} @roleName={{this.model.roleName}} />
|
||||
{{else}}
|
||||
<GenerateCredentials
|
||||
@backendPath={{this.model.backendPath}}
|
||||
|
|
|
|||
|
|
@ -1,115 +1,6 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
Copyright IBM Corp. 2016, 2026
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Page::Header @title="Sign SSH Key">
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array
|
||||
(hash label="SSH" route="vault.cluster.secrets.backend" model=this.backend.id)
|
||||
(hash label="Sign" route="vault.cluster.secrets.backend" model=this.backend.id)
|
||||
(hash label=this.model.role.name route="vault.cluster.secrets.backend.show" model=this.model.role.name)
|
||||
(hash label="Sign SSH Key")
|
||||
}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
{{#if this.model.signedKey}}
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
<Hds::Alert @type="inline" @color="warning" class="has-top-margin-s has-bottom-margin-s" as |A|>
|
||||
<A.Title>Warning</A.Title>
|
||||
<A.Description>
|
||||
You will not be able to access this information later, so please copy the information below.
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
{{#each this.model.attrs as |attr|}}
|
||||
{{#if (eq attr.type "object")}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{stringify (get this.model attr.name)}}
|
||||
/>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{get this.model attr.name}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<Hds::Copy::Button
|
||||
@text="Copy key"
|
||||
@textToCopy={{this.model.signedKey}}
|
||||
@onError={{(fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger"))}}
|
||||
class="primary"
|
||||
/>
|
||||
</div>
|
||||
{{#if this.model.leaseId}}
|
||||
<div class="control">
|
||||
<Hds::Copy::Button
|
||||
@text="Copy lease ID"
|
||||
@textToCopy={{this.model.leaseId}}
|
||||
@onError={{(fn
|
||||
(set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger")
|
||||
)}}
|
||||
class="secondary"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="control">
|
||||
<Hds::Button @text="Back" @color="secondary" {{on "click" (action "newModel")}} data-test-back-button />
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<form {{action "sign" on="submit"}} data-test-secret-generate-form="true">
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @model={{this.model}} />
|
||||
<NamespaceReminder @mode="sign" @noun="SSH key" />
|
||||
{{#if this.model.attrs}}
|
||||
{{#let (find-by "name" "publicKey" this.model.attrs) as |attr|}}
|
||||
<FormFieldFromModel @attr={{attr}} @model={{this.model}} />
|
||||
{{/let}}
|
||||
{{! valid_principals is required unless allow_empty_principals is true (not recommended) }}
|
||||
{{#let (find-by "name" "validPrincipals" this.model.attrs) as |attr|}}
|
||||
<FormFieldFromModel @attr={{attr}} @model={{this.model}} />
|
||||
{{/let}}
|
||||
<ToggleButton @isOpen={{this.showOptions}} @onClick={{fn (mut this.showOptions)}} data-test-toggle-button />
|
||||
{{#if this.showOptions}}
|
||||
<div class="box is-marginless">
|
||||
{{#each this.model.attrs as |attr|}}
|
||||
{{! These attrs render above, outside of the "More options" toggle }}
|
||||
{{#if (not (includes attr.name (array "publicKey" "validPrincipals")))}}
|
||||
<FormFieldFromModel
|
||||
@attr={{attr}}
|
||||
@model={{this.model}}
|
||||
@updateTtl={{action "updateTtl" attr.name}}
|
||||
@emptyData={{this.emptyData}}
|
||||
@editorUpdated={{action "editorUpdated" attr.name}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<Hds::ButtonSet class="has-top-bottom-margin">
|
||||
<Hds::Button
|
||||
@text="Sign"
|
||||
@icon={{if this.loading "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.loading}}
|
||||
data-test-submit
|
||||
/>
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route="vault.cluster.secrets.backend.list-root"
|
||||
@model={{this.backend.id}}
|
||||
data-test-cancel
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</form>
|
||||
{{/if}}
|
||||
<SshSignKey @roleName={{this.model.roleName}} @backendPath={{this.model.backendPath}} />
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
|
|
@ -52,8 +52,8 @@ module('Acceptance | ssh | roles', function (hooks) {
|
|||
await click(GENERAL.inputByAttr('allow_empty_principals'));
|
||||
},
|
||||
async fillInGenerate() {
|
||||
await fillIn(GENERAL.inputByAttr('publicKey'), PUB_KEY);
|
||||
await click('[data-test-toggle-button]');
|
||||
await fillIn(GENERAL.inputByAttr('public_key'), PUB_KEY);
|
||||
await click(GENERAL.button('More options'));
|
||||
|
||||
await click(GENERAL.ttl.toggle('TTL'));
|
||||
await fillIn(GENERAL.selectByAttr('ttl-unit'), 'm');
|
||||
|
|
|
|||
Loading…
Reference in a new issue