UI: PKI Read Role Details (#17985)

This commit is contained in:
Chelsea Shaw 2022-11-21 14:09:04 -06:00 committed by GitHub
parent d392754914
commit 1c0b2df8f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 258 additions and 9 deletions

View File

@ -25,3 +25,6 @@
/package-lock.json.ember-try /package-lock.json.ember-try
/yarn.lock.ember-try /yarn.lock.ember-try
/tests/helpers/vault-keys.js /tests/helpers/vault-keys.js
# typescript declaration files
*.d.ts

View File

@ -51,4 +51,8 @@ export default class PkiRoleAdapter extends ApplicationAdapter {
query(store, type, query) { query(store, type, query) {
return this.fetchByQuery(store, query); return this.fetchByQuery(store, query);
} }
queryRecord(store, type, query) {
return this.fetchByQuery(store, query);
}
} }

View File

@ -29,6 +29,7 @@ export default class PkiRoleModel extends Model {
@attr('string', { @attr('string', {
label: 'Issuer reference', label: 'Issuer reference',
detailsLabel: 'Issuer',
defaultValue: 'default', defaultValue: 'default',
subText: `Specifies the issuer that will be used to create certificates with this role. To find this, run read -field=default pki_int/config/issuers in the console. By default, we will use the mounts default issuer.`, subText: `Specifies the issuer that will be used to create certificates with this role. To find this, run read -field=default pki_int/config/issuers in the console. By default, we will use the mounts default issuer.`,
}) })
@ -36,6 +37,7 @@ export default class PkiRoleModel extends Model {
@attr({ @attr({
label: 'Not valid after', label: 'Not valid after',
detailsLabel: 'Issued certificates expire after',
subText: subText:
'The time after which this certificate will no longer be valid. This can be a TTL (a range of time from now) or a specific date. If no TTL is set, the system uses "default" or the value of max_ttl, whichever is shorter. Alternatively, you can set the not_after date below.', 'The time after which this certificate will no longer be valid. This can be a TTL (a range of time from now) or a specific date. If no TTL is set, the system uses "default" or the value of max_ttl, whichever is shorter. Alternatively, you can set the not_after date below.',
editType: 'yield', editType: 'yield',
@ -44,6 +46,7 @@ export default class PkiRoleModel extends Model {
@attr({ @attr({
label: 'Backdate validity', label: 'Backdate validity',
detailsLabel: 'Issued certificate backdating',
helperTextEnabled: helperTextEnabled:
'Also called the not_before_duration property. Allows certificates to be valid for a certain time period before now. This is useful to correct clock misalignment on various systems when setting up your CA.', 'Also called the not_before_duration property. Allows certificates to be valid for a certain time period before now. This is useful to correct clock misalignment on various systems when setting up your CA.',
editType: 'ttl', editType: 'ttl',
@ -57,6 +60,7 @@ export default class PkiRoleModel extends Model {
helperTextDisabled: helperTextDisabled:
'The maximum Time-To-Live of certificates generated by this role. If not set, the system max lease TTL will be used.', 'The maximum Time-To-Live of certificates generated by this role. If not set, the system max lease TTL will be used.',
editType: 'ttl', editType: 'ttl',
defaultShown: 'System default',
}) })
maxTtl; maxTtl;
@ -71,6 +75,7 @@ export default class PkiRoleModel extends Model {
@attr('boolean', { @attr('boolean', {
label: 'Do not store certificates in storage backend', label: 'Do not store certificates in storage backend',
detailsLabel: 'Store in storage backend', // template reverses value
subText: subText:
'This can improve performance when issuing large numbers of certificates. However, certificates issued in this way cannot be enumerated or revoked.', 'This can improve performance when issuing large numbers of certificates. However, certificates issued in this way cannot be enumerated or revoked.',
editType: 'boolean', editType: 'boolean',
@ -80,6 +85,7 @@ export default class PkiRoleModel extends Model {
@attr('boolean', { @attr('boolean', {
label: 'Basic constraints valid for non-CA', label: 'Basic constraints valid for non-CA',
detailsLabel: 'Add basic constraints',
subText: 'Mark Basic Constraints valid when issuing non-CA certificates.', subText: 'Mark Basic Constraints valid when issuing non-CA certificates.',
editType: 'boolean', editType: 'boolean',
}) })
@ -231,16 +237,20 @@ export default class PkiRoleModel extends Model {
defaultValue() { defaultValue() {
return ['DigitalSignature', 'KeyAgreement', 'KeyEncipherment']; return ['DigitalSignature', 'KeyAgreement', 'KeyEncipherment'];
}, },
defaultShown: 'None',
}) })
keyUsage; keyUsage;
@attr('array', { @attr('array', {
defaultValue() { defaultShown: 'None',
return [];
},
}) })
extKeyUsage; extKeyUsage;
@attr('array', {
defaultShown: 'None',
})
extKeyUsageOids;
@attr({ hideFormSection: true }) organization; @attr({ hideFormSection: true }) organization;
@attr({ hideFormSection: true }) country; @attr({ hideFormSection: true }) country;
@attr({ hideFormSection: true }) locality; @attr({ hideFormSection: true }) locality;
@ -332,7 +342,7 @@ export default class PkiRoleModel extends Model {
'Key parameters': ['keyType', 'keyBits', 'signatureBits'], 'Key parameters': ['keyType', 'keyBits', 'signatureBits'],
}, },
{ {
'Key usage': ['keyUsage', 'extKeyUsage'], 'Key usage': ['keyUsage', 'extKeyUsage', 'extKeyUsageOids'],
}, },
{ 'Policy identifiers': ['policyIdentifiers'] }, { 'Policy identifiers': ['policyIdentifiers'] },
{ {

View File

@ -0,0 +1,98 @@
<PageHeader as |p|>
<p.top>
{{! TODO: This should be replaced with HDS::Breadcrumbs }}
<nav class="key-value-header breadcrumb" aria-label="breadcrumbs" data-test-breadcrumbs="role-details">
<ul>
<li>
<span class="sep">/</span>
<LinkToExternal @route="secrets">secrets</LinkToExternal>
</li>
{{#each this.breadcrumbs as |breadcrumb|}}
<li>
<span class="sep">/</span>
{{#if breadcrumb.path}}
<LinkTo @route={{breadcrumb.path}}>
{{breadcrumb.label}}
</LinkTo>
{{else}}
{{breadcrumb.label}}
{{/if}}
</li>
{{/each}}
</ul>
</nav>
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-role-details-title>
<Icon @name="file-text" @size="24" class="has-text-grey-light" />
PKI Role
<code>{{@role.name}}</code>
</h1>
</p.levelLeft>
</PageHeader>
<Toolbar>
<ToolbarActions>
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{this.deleteRole}}
@confirmTitle="Delete role?"
@confirmButtonText="Delete"
data-test-pki-role-delete
>
Delete
</ConfirmAction>
<div class="toolbar-separator"></div>
<LinkTo class="toolbar-link" @route="overview">Generate Certificate <Icon @name="chevron-right" /></LinkTo>
<LinkTo class="toolbar-link" @route="overview">Sign Certificate <Icon @name="chevron-right" /></LinkTo>
<LinkTo class="toolbar-link" @route="roles.role.edit" @model={{@role.id}}>Edit <Icon @name="chevron-right" /></LinkTo>
</ToolbarActions>
</Toolbar>
{{#each @role.fieldGroups as |fg|}}
{{#each-in fg as |group fields|}}
{{#if (not-eq group "default")}}
<h3 class="is-size-4 has-text-semibold has-top-margin-m">{{group}}</h3>
{{/if}}
{{#each fields as |attr|}}
{{#let (get @role attr.name) as |val|}}
{{#if (eq attr.name "issuerRef")}}
<InfoTableRow
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
@value={{val}}
@alwaysRender={{true}}
>
<LinkTo @route="issuers.issuer.details" @model={{val}}>{{val}}</LinkTo>
</InfoTableRow>
{{else if (includes attr.name this.arrayAttrs)}}
<InfoTableRow
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
@value={{val}}
@alwaysRender={{true}}
>
{{#if (gt val.length 0)}}
{{#each val as |key|}}
<span>{{key}},</span>
{{/each}}
{{else}}
None
{{/if}}
</InfoTableRow>
{{else if (eq attr.name "noStore")}}
<InfoTableRow
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
@value={{not val}}
@alwaysRender={{true}}
/>
{{else}}
<InfoTableRow
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
@value={{val}}
@alwaysRender={{true}}
@formatDate={{eq attr.name "customTtl"}}
@type={{or attr.type attr.options.type}}
@defaultShown={{attr.options.defaultShown}}
/>
{{/if}}
{{/let}}
{{/each}}
{{/each-in}}
{{/each}}

View File

@ -0,0 +1,35 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
// interface Attribute {
// name: string;
// options?: {
// label?: string;
// };
// }
// TODO: pull this in from route model once it's TS
interface Args {
role: {
backend: string;
id: string;
};
}
export default class DetailsPage extends Component<Args> {
get breadcrumbs() {
return [
{ label: this.args.role.backend || 'pki', path: 'overview' },
{ label: 'roles', path: 'roles.index' },
{ label: this.args.role.id },
];
}
get arrayAttrs() {
return ['keyUsage', 'extKeyUsage', 'extKeyUsageOids'];
}
@action deleteRole() {
// TODO: delete role
}
}

View File

@ -7,7 +7,8 @@ export default class PkiRolesIndexRoute extends Route {
@service pathHelp; @service pathHelp;
beforeModel() { beforeModel() {
// Must call this promise before the model hook otherwise it doesn't add OpenApi to record. // Must call this promise before the model hook otherwise
// the model doesn't hydrate from OpenAPI correctly.
return this.pathHelp.getNewModel('pki/role', 'pki'); return this.pathHelp.getNewModel('pki/role', 'pki');
} }

View File

@ -1,3 +1,11 @@
import Route from '@ember/routing/route'; import PkiRolesIndexRoute from '../index';
export default class PkiRoleDetailsRoute extends Route {} export default class RolesRoleDetailsRoute extends PkiRolesIndexRoute {
model() {
const { role } = this.paramsFor('roles/role');
return this.store.queryRecord('pki/role', {
backend: this.secretMountPath.currentPath,
id: role,
});
}
}

View File

@ -1 +1 @@
roles.role.details <PkiRoleDetailsPage @role={{this.model}} />

View File

@ -7,7 +7,35 @@
"dependencies": { "dependencies": {
"ember-cli-babel": "*", "ember-cli-babel": "*",
"ember-cli-htmlbars": "*", "ember-cli-htmlbars": "*",
"ember-cli-typescript": "*" "ember-cli-typescript": "*",
"@types/ember": "latest",
"@types/ember-data": "latest",
"@types/ember-data__adapter": "latest",
"@types/ember-data__model": "latest",
"@types/ember-data__serializer": "latest",
"@types/ember-data__store": "latest",
"@types/ember-qunit": "latest",
"@types/ember-resolver": "latest",
"@types/ember__application": "latest",
"@types/ember__array": "latest",
"@types/ember__component": "latest",
"@types/ember__controller": "latest",
"@types/ember__debug": "latest",
"@types/ember__destroyable": "latest",
"@types/ember__engine": "latest",
"@types/ember__error": "latest",
"@types/ember__object": "latest",
"@types/ember__polyfills": "latest",
"@types/ember__routing": "latest",
"@types/ember__runloop": "latest",
"@types/ember__service": "latest",
"@types/ember__string": "latest",
"@types/ember__template": "latest",
"@types/ember__test": "latest",
"@types/ember__test-helpers": "latest",
"@types/ember__utils": "latest",
"@types/qunit": "latest",
"@types/rsvp": "latest"
}, },
"ember-addon": { "ember-addon": {
"paths": [ "paths": [

View File

@ -0,0 +1,9 @@
export const SELECTORS = {
breadcrumbContainer: '[data-test-breadcrumbs="role-details"]',
breadcrumbs: '[data-test-breadcrumbs="role-details"] li',
title: '[data-test-role-details-title]',
issuerLabel: '[data-test-row-label="Issuer"]',
noStoreValue: '[data-test-value-div="Store in storage backend"]',
keyUsageValue: '[data-test-value-div="Key usage"]',
extKeyUsageValue: '[data-test-value-div="Ext key usage"]',
};

View File

@ -0,0 +1,42 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { SELECTORS } from 'vault/tests/helpers/pki/page-role-details';
module('Integration | Component | pki role details page', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.model = this.store.createRecord('pki/role', {
name: 'Foobar',
noStore: false,
keyUsage: [],
extKeyUsage: ['bar', 'baz'],
});
this.model.backend = 'pki';
});
test('it should render the page component', async function (assert) {
assert.expect(7);
await render(
hbs`
<PkiRoleDetailsPage @role={{this.model}} />
`,
{ owner: this.engine }
);
assert.dom(SELECTORS.breadcrumbContainer).exists({ count: 1 }, 'breadcrumb containers exist');
assert.dom(SELECTORS.breadcrumbs).exists({ count: 4 }, 'Shows 4 breadcrumbs');
assert.dom(SELECTORS.title).containsText('PKI Role Foobar', 'Title includes type and name of role');
// Attribute-specific checks
assert.dom(SELECTORS.issuerLabel).hasText('Issuer', 'Label is');
assert.dom(SELECTORS.keyUsageValue).hasText('None', 'Key usage shows none when array is empty');
assert
.dom(SELECTORS.extKeyUsageValue)
.hasText('bar, baz,', 'Key usage shows comma-joined values when array has items');
assert.dom(SELECTORS.noStoreValue).containsText('Yes', 'noStore shows opposite of what the value is');
});
});

View File

@ -1,6 +1,17 @@
{ {
"extends": "@tsconfig/ember/tsconfig.json", "extends": "@tsconfig/ember/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"allowJs": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noEmitOnError": true,
"skipLibCheck": true,
// The combination of `baseUrl` with `paths` allows Ember's classic package // The combination of `baseUrl` with `paths` allows Ember's classic package
// layout, which is not resolvable with the Node resolution algorithm, to // layout, which is not resolvable with the Node resolution algorithm, to
// work with TypeScript. // work with TypeScript.