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'] },
|
||||
{
|
||||
'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',
|
||||
|
|
|
@ -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 { getOwner } from '@ember/application';
|
||||
|
||||
export default class BlogPostController extends Controller {
|
||||
export default class PkiRolesIndexController extends Controller {
|
||||
get mountPoint() {
|
||||
return getOwner(this).mountPoint;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
}}
|
||||
@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-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>
|
||||
|
|
Loading…
Reference in New Issue