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
/yarn.lock.ember-try
/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) {
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', {
label: 'Issuer reference',
detailsLabel: 'Issuer',
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.`,
})
@ -36,6 +37,7 @@ export default class PkiRoleModel extends Model {
@attr({
label: 'Not valid after',
detailsLabel: 'Issued certificates expire after',
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.',
editType: 'yield',
@ -44,6 +46,7 @@ export default class PkiRoleModel extends Model {
@attr({
label: 'Backdate validity',
detailsLabel: 'Issued certificate backdating',
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.',
editType: 'ttl',
@ -57,6 +60,7 @@ export default class PkiRoleModel extends Model {
helperTextDisabled:
'The maximum Time-To-Live of certificates generated by this role. If not set, the system max lease TTL will be used.',
editType: 'ttl',
defaultShown: 'System default',
})
maxTtl;
@ -71,6 +75,7 @@ export default class PkiRoleModel extends Model {
@attr('boolean', {
label: 'Do not store certificates in storage backend',
detailsLabel: 'Store in storage backend', // template reverses value
subText:
'This can improve performance when issuing large numbers of certificates. However, certificates issued in this way cannot be enumerated or revoked.',
editType: 'boolean',
@ -80,6 +85,7 @@ export default class PkiRoleModel extends Model {
@attr('boolean', {
label: 'Basic constraints valid for non-CA',
detailsLabel: 'Add basic constraints',
subText: 'Mark Basic Constraints valid when issuing non-CA certificates.',
editType: 'boolean',
})
@ -231,16 +237,20 @@ export default class PkiRoleModel extends Model {
defaultValue() {
return ['DigitalSignature', 'KeyAgreement', 'KeyEncipherment'];
},
defaultShown: 'None',
})
keyUsage;
@attr('array', {
defaultValue() {
return [];
},
defaultShown: 'None',
})
extKeyUsage;
@attr('array', {
defaultShown: 'None',
})
extKeyUsageOids;
@attr({ hideFormSection: true }) organization;
@attr({ hideFormSection: true }) country;
@attr({ hideFormSection: true }) locality;
@ -332,7 +342,7 @@ export default class PkiRoleModel extends Model {
'Key parameters': ['keyType', 'keyBits', 'signatureBits'],
},
{
'Key usage': ['keyUsage', 'extKeyUsage'],
'Key usage': ['keyUsage', 'extKeyUsage', 'extKeyUsageOids'],
},
{ '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;
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');
}

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": {
"ember-cli-babel": "*",
"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": {
"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",
"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
// layout, which is not resolvable with the Node resolution algorithm, to
// work with TypeScript.