PKI Issuer List view (#17210)

* initial setup for issuers toolbar and some slight changes to roles model after discussion with design.

* wip

* wip ... :/

* finalizes serializer and linkedblock iteration of is_default

* clean up

* fix

* forgot this bit

* pr comments amendments:

* small PR comment changes
This commit is contained in:
Angel Garbarino 2022-09-20 08:25:57 -07:00 committed by GitHub
parent 559754d580
commit 2e197fcfcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 233 additions and 25 deletions

View File

@ -0,0 +1,28 @@
import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default class PkiIssuerEngineAdapter extends ApplicationAdapter {
namespace = 'v1';
optionsForQuery(id) {
let data = {};
if (!id) {
data['list'] = true;
}
return { data };
}
urlForQuery(backend, id) {
let url = `${this.buildURL()}/${encodePath(backend)}/issuers`;
if (id) {
url = url + '/' + encodePath(id);
}
return url;
}
async query(store, type, query) {
const { backend, id } = query;
let response = await this.ajax(this.urlForQuery(backend, id), 'GET', this.optionsForQuery(id));
return response;
}
}

View File

@ -0,0 +1,51 @@
import Model, { attr } from '@ember-data/model';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import { withModelValidations } from 'vault/decorators/model-validations';
const validations = {
name: [
{ type: 'presence', message: 'Name is required.' },
{
type: 'containsWhiteSpace',
message: 'Name cannot contain whitespace.',
},
],
};
@withModelValidations(validations)
export default class PkiIssuersEngineModel extends Model {
@attr('string', { readOnly: true }) backend;
@attr('string', {
label: 'Issuer name',
fieldValue: 'id',
})
name;
get useOpenAPI() {
return true;
}
getHelpUrl(backend) {
return `/v1/${backend}/issuer/example?help=1`;
}
@attr('boolean') isDefault;
@attr('string') issuerName;
// Form Fields not hidden in toggle options
_attributeMeta = null;
get formFields() {
if (!this._attributeMeta) {
this._attributeMeta = expandAttributeMeta(this, [
'name',
'leafNotAfterBehavior',
'usage',
'manualChain',
'issuingCertifications',
'crlDistributionPoints',
'ocspServers',
'deltaCrlUrls', // new endpoint, mentioned in RFC, but need to confirm it's there.
]);
}
return this._attributeMeta;
}
}

View File

@ -75,6 +75,7 @@ export default class PkiRolesEngineModel extends Model {
{ default: ['name'] },
{
'Domain handling': [
'allowedDomains',
'allowedDomainTemplate',
'allowBareDomains',
'allowSubdomains',
@ -93,21 +94,19 @@ export default class PkiRolesEngineModel extends Model {
'DigitalSignature', // ARG TODO: capitalized in the docs, but should confirm
'KeyAgreement',
'KeyEncipherment',
'extKeyUsage', // ARG TODO: takes a list, but we have these as checkboxes from the options on the golang site: https://pkg.go.dev/crypto/x509#ExtKeyUsage
],
},
{ 'Policy identifiers': ['policy_identifiers'] },
{ 'Policy identifiers': ['policyIdentifiers'] },
{
'Subject Alternative Name (SAN) Options': [
'allow_ip_sans',
'allowed_uri_sans',
'allowed_other_sans',
],
'Subject Alternative Name (SAN) Options': ['allowIpSans', 'allowedUriSans', 'allowedOtherSans'],
},
{
'Additional subject fields': [
'allowed_serial_numbers',
'require_cn',
'use_csr_common_name',
'requireCn',
'useCsrCommonName',
'useCsrSans',
'ou',
'organization',
'country',

View File

@ -0,0 +1,15 @@
import ApplicationSerializer from '../application';
export default class PkiIssuerEngineSerializer extends ApplicationSerializer {
// rehydrate each issuer model so all model attributes are accessible from the LIST response
normalizeItems(payload) {
if (payload.data) {
if (payload.data?.keys && Array.isArray(payload.data.keys)) {
return payload.data.keys.map((key) => ({ id: key, ...payload.data.key_info[key] }));
}
Object.assign(payload, payload.data);
delete payload.data;
}
return payload;
}
}

View File

@ -0,0 +1,10 @@
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { next } from '@ember/runloop';
export default class PkiRolesIssuerController extends Controller {
// To prevent production build bug of passing D.actions to on "click": https://github.com/hashicorp/vault/pull/16983
@action onLinkClick(D) {
next(() => D.actions.close());
}
}

View File

@ -1,7 +1,7 @@
import Controller from '@ember/controller';
import { getOwner } from '@ember/application';
export default class BlogPostController extends Controller {
export default class PkiRolesIndexController extends Controller {
get mountPoint() {
return getOwner(this).mountPoint;
}

View File

@ -15,7 +15,7 @@ export default buildRoutes(function () {
this.route('details');
});
this.route('roles', function () {
this.route('index', { path: '/' }); // ARG TODO remove
this.route('index', { path: '/' });
this.route('create');
this.route('role', { path: '/:id' }, function () {
this.route('details');
@ -23,7 +23,7 @@ export default buildRoutes(function () {
});
});
this.route('issuers', function () {
this.route('create');
this.route('index', { path: '/' });
this.route('issuer', { path: '/:id' }, function () {
this.route('details');
this.route('edit');

View File

@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class PkiConfigurationCreateGenerateCsrRoute extends Route {}

View File

@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class PkiConfigurationCreateGenerateRootRoute extends Route {}

View File

@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class PkiConfigurationCreateImportCaRoute extends Route {}

View File

@ -0,0 +1,23 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class PkiIssuersIndexRoute extends Route {
@service store;
@service secretMountPath;
@service pathHelp;
model() {
// the pathHelp service is needed for adding openAPI to the model
this.pathHelp.getNewModel('pki/pki-issuer-engine', 'pki');
return this.store
.query('pki/pki-issuer-engine', { backend: this.secretMountPath.currentPath })
.catch((err) => {
if (err.httpStatus === 404) {
return [];
} else {
throw err;
}
});
}
}

View File

@ -0,0 +1 @@
configuration.create.generate-csr

View File

@ -0,0 +1 @@
configuration.create.generate-root

View File

@ -0,0 +1 @@
configuration.create.import-ca

View File

@ -8,3 +8,4 @@
}}
@isEngine={{true}}
/>
{{outlet}}

View File

@ -0,0 +1,79 @@
<Toolbar>
<ToolbarActions>
<ToolbarLink @params={{array "configuration.create.import-ca"}}>
Import
</ToolbarLink>
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
<D.Trigger class={{concat "toolbar-link" (if D.isOpen " is-active")}} @htmlTag="button">
Generate
<Chevron @direction="down" @isButton={{true}} />
</D.Trigger>
<D.Content @defaultClass="popup-menu-content">
<nav class="box menu">
<ul class="menu-list">
<li class="action">
<LinkTo @route="configuration.create.generate-root" {{on "click" (fn this.onLinkClick D)}}>
Root
</LinkTo>
</li>
<li class="action">
<LinkTo @route="configuration.create.generate-csr" {{on "click" (fn this.onLinkClick D)}}>
Intermediate CSR
</LinkTo>
</li>
</ul>
</nav>
</D.Content>
</BasicDropdown>
</ToolbarActions>
</Toolbar>
{{#if (gt this.model.length 0)}}
{{#each this.model as |model|}}
<LinkedBlock class="list-item-row" @params={{array "roles.role.details" model.id}} @linkPrefix={{this.mountPoint}}>
<div class="level is-mobile">
<div class="level-left">
<div>
<Icon @name="certificate" class="has-text-grey-light" />
<span class="has-text-weight-semibold is-underline">
{{or model.issuerName model.id}}
</span>
<div class="is-flex-row has-left-margin-l has-top-margin-xs">
{{#if model.isDefault}}
<span class="tag has-text-grey-dark">default issuer</span>
{{/if}}
{{#if model.issuerName}}
<span class="tag has-text-grey-dark">{{model.id}}</span>
{{/if}}
</div>
</div>
</div>
<div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
<PopupMenu>
<nav class="menu">
<ul class="menu-list">
<li>
<LinkTo @route="roles.role.details" @model={{model.id}}>
Details
</LinkTo>
</li>
<li>
<LinkTo @route="roles.role.edit" @model={{model.id}}>
Edit
</LinkTo>
</li>
</ul>
</nav>
</PopupMenu>
</div>
</div>
</div>
</LinkedBlock>
{{/each}}
{{else}}
<EmptyState @title="PKI not configured" @message="This PKI mount hasnt yet been configured with a certificate issuer.">
<LinkTo @route="configuration.create.index" @model={{this.model}}>
Configure PKI
</LinkTo>
</EmptyState>
{{/if}}

View File

@ -12,7 +12,7 @@
<div class="level is-mobile">
<div class="level-left">
<div>
<Icon @name="file" class="has-text-grey-light" />
<Icon @name="user" class="has-text-grey-light" />
<span class="has-text-weight-semibold is-underline">
{{model.id}}
</span>
@ -24,22 +24,12 @@
<nav class="menu">
<ul class="menu-list">
<li>
<LinkTo
@route="roles.role.details"
@model={{model.id}}
{{! ARG TODO return with permissions }}
{{!-- @disabled={{eq model.canRead false}} --}}
>
<LinkTo @route="roles.role.details" @model={{model.id}}>
Details
</LinkTo>
</li>
<li>
<LinkTo
@route="roles.role.edit"
@model={{model.id}}
{{! ARG TODO return with permissions }}
{{!-- @disabled={{eq model.canEdit false}} --}}
>
<LinkTo @route="roles.role.edit" @model={{model.id}}>
Edit
</LinkTo>
</li>