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:
parent
559754d580
commit
2e197fcfcd
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,6 +75,7 @@ export default class PkiRolesEngineModel extends Model {
|
||||||
{ default: ['name'] },
|
{ default: ['name'] },
|
||||||
{
|
{
|
||||||
'Domain handling': [
|
'Domain handling': [
|
||||||
|
'allowedDomains',
|
||||||
'allowedDomainTemplate',
|
'allowedDomainTemplate',
|
||||||
'allowBareDomains',
|
'allowBareDomains',
|
||||||
'allowSubdomains',
|
'allowSubdomains',
|
||||||
|
@ -93,21 +94,19 @@ export default class PkiRolesEngineModel extends Model {
|
||||||
'DigitalSignature', // ARG TODO: capitalized in the docs, but should confirm
|
'DigitalSignature', // ARG TODO: capitalized in the docs, but should confirm
|
||||||
'KeyAgreement',
|
'KeyAgreement',
|
||||||
'KeyEncipherment',
|
'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': [
|
'Subject Alternative Name (SAN) Options': ['allowIpSans', 'allowedUriSans', 'allowedOtherSans'],
|
||||||
'allow_ip_sans',
|
|
||||||
'allowed_uri_sans',
|
|
||||||
'allowed_other_sans',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'Additional subject fields': [
|
'Additional subject fields': [
|
||||||
'allowed_serial_numbers',
|
'allowed_serial_numbers',
|
||||||
'require_cn',
|
'requireCn',
|
||||||
'use_csr_common_name',
|
'useCsrCommonName',
|
||||||
|
'useCsrSans',
|
||||||
'ou',
|
'ou',
|
||||||
'organization',
|
'organization',
|
||||||
'country',
|
'country',
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { getOwner } from '@ember/application';
|
import { getOwner } from '@ember/application';
|
||||||
|
|
||||||
export default class BlogPostController extends Controller {
|
export default class PkiRolesIndexController extends Controller {
|
||||||
get mountPoint() {
|
get mountPoint() {
|
||||||
return getOwner(this).mountPoint;
|
return getOwner(this).mountPoint;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default buildRoutes(function () {
|
||||||
this.route('details');
|
this.route('details');
|
||||||
});
|
});
|
||||||
this.route('roles', function () {
|
this.route('roles', function () {
|
||||||
this.route('index', { path: '/' }); // ARG TODO remove
|
this.route('index', { path: '/' });
|
||||||
this.route('create');
|
this.route('create');
|
||||||
this.route('role', { path: '/:id' }, function () {
|
this.route('role', { path: '/:id' }, function () {
|
||||||
this.route('details');
|
this.route('details');
|
||||||
|
@ -23,7 +23,7 @@ export default buildRoutes(function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.route('issuers', function () {
|
this.route('issuers', function () {
|
||||||
this.route('create');
|
this.route('index', { path: '/' });
|
||||||
this.route('issuer', { path: '/:id' }, function () {
|
this.route('issuer', { path: '/:id' }, function () {
|
||||||
this.route('details');
|
this.route('details');
|
||||||
this.route('edit');
|
this.route('edit');
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class PkiConfigurationCreateGenerateCsrRoute extends Route {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class PkiConfigurationCreateGenerateRootRoute extends Route {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class PkiConfigurationCreateImportCaRoute extends Route {}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
configuration.create.generate-csr
|
|
@ -0,0 +1 @@
|
||||||
|
configuration.create.generate-root
|
|
@ -0,0 +1 @@
|
||||||
|
configuration.create.import-ca
|
|
@ -7,4 +7,5 @@
|
||||||
model=this.model.id
|
model=this.model.id
|
||||||
}}
|
}}
|
||||||
@isEngine={{true}}
|
@isEngine={{true}}
|
||||||
/>
|
/>
|
||||||
|
{{outlet}}
|
|
@ -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 hasn’t yet been configured with a certificate issuer.">
|
||||||
|
<LinkTo @route="configuration.create.index" @model={{this.model}}>
|
||||||
|
Configure PKI
|
||||||
|
</LinkTo>
|
||||||
|
</EmptyState>
|
||||||
|
{{/if}}
|
|
@ -12,7 +12,7 @@
|
||||||
<div class="level is-mobile">
|
<div class="level is-mobile">
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<div>
|
<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">
|
<span class="has-text-weight-semibold is-underline">
|
||||||
{{model.id}}
|
{{model.id}}
|
||||||
</span>
|
</span>
|
||||||
|
@ -24,22 +24,12 @@
|
||||||
<nav class="menu">
|
<nav class="menu">
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
<LinkTo
|
<LinkTo @route="roles.role.details" @model={{model.id}}>
|
||||||
@route="roles.role.details"
|
|
||||||
@model={{model.id}}
|
|
||||||
{{! ARG TODO return with permissions }}
|
|
||||||
{{!-- @disabled={{eq model.canRead false}} --}}
|
|
||||||
>
|
|
||||||
Details
|
Details
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<LinkTo
|
<LinkTo @route="roles.role.edit" @model={{model.id}}>
|
||||||
@route="roles.role.edit"
|
|
||||||
@model={{model.id}}
|
|
||||||
{{! ARG TODO return with permissions }}
|
|
||||||
{{!-- @disabled={{eq model.canEdit false}} --}}
|
|
||||||
>
|
|
||||||
Edit
|
Edit
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Reference in New Issue