diff --git a/ui/app/adapters/pki/issuer.js b/ui/app/adapters/pki/issuer.js index 90560a359109e..4c0a5b3b6ca3a 100644 --- a/ui/app/adapters/pki/issuer.js +++ b/ui/app/adapters/pki/issuer.js @@ -13,14 +13,19 @@ export default class PkiIssuerAdapter extends ApplicationAdapter { } urlForQuery(backend, id) { - let url = `${this.buildURL()}/${encodePath(backend)}/issuers`; + const baseUrl = `${this.buildURL()}/${encodePath(backend)}`; if (id) { - url = url + '/' + encodePath(id); + return `${baseUrl}/issuer/${encodePath(id)}`; + } else { + return `${baseUrl}/issuers`; } - return url; } query(store, type, query) { + return this.ajax(this.urlForQuery(query.backend), 'GET', this.optionsForQuery()); + } + + queryRecord(store, type, query) { const { backend, id } = query; return this.ajax(this.urlForQuery(backend, id), 'GET', this.optionsForQuery(id)); } diff --git a/ui/app/models/pki/issuer.js b/ui/app/models/pki/issuer.js index fbe9bb9fd898c..9e94d6a25d5f0 100644 --- a/ui/app/models/pki/issuer.js +++ b/ui/app/models/pki/issuer.js @@ -1,51 +1,67 @@ -import Model, { attr } from '@ember-data/model'; -import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; -import { withModelValidations } from 'vault/decorators/model-validations'; +import PkiCertificateBaseModel from './certificate/base'; +import { attr } from '@ember-data/model'; +import { withFormFields } from 'vault/decorators/model-form-fields'; +import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; -const validations = { - name: [ - { type: 'presence', message: 'Name is required.' }, - { - type: 'containsWhiteSpace', - message: 'Name cannot contain whitespace.', - }, - ], -}; +@withFormFields(null, [ + { + default: [ + 'certificate', + 'caChain', + 'commonName', + 'issuerName', + 'notValidBefore', + 'serialNumber', + 'keyId', + 'uriSans', + 'notValidAfter', + ], + }, + { 'Issuer URLs': ['issuingCertificates', 'crlDistributionPoints', 'ocspServers', 'deltaCrlUrls'] }, +]) +export default class PkiIssuerModel extends PkiCertificateBaseModel { + getHelpUrl(backend) { + return `/v1/${backend}/issuer/example?help=1`; + } + + @attr('string') issuerId; + @attr('string', { displayType: 'masked' }) certificate; + @attr('string', { displayType: 'masked', label: 'CA Chain' }) caChain; + @attr('date', { + label: 'Issue date', + }) + notValidBefore; -@withModelValidations(validations) -export default class PkiIssuerModel extends Model { - @attr('string', { readOnly: true }) backend; @attr('string', { - label: 'Issuer name', - fieldValue: 'id', + label: 'Default key ID', + }) + keyId; + + @attr({ + label: 'Subject Alternative Names', }) - name; + uriSans; - get useOpenAPI() { - return true; + @lazyCapabilities(apiPath`${'backend'}/issuer/${'issuerId'}`) issuerPath; + @lazyCapabilities(apiPath`${'backend'}/root/rotate/exported`) rotateExported; + @lazyCapabilities(apiPath`${'backend'}/root/rotate/internal`) rotateInternal; + @lazyCapabilities(apiPath`${'backend'}/root/rotate/existing`) rotateExisting; + @lazyCapabilities(apiPath`${'backend'}/intermediate/cross-sign`) crossSignPath; + @lazyCapabilities(apiPath`${'backend'}/issuer/${'issuerId'}/sign-intermediate`) signIntermediate; + get canRotateIssuer() { + return ( + this.rotateExported.get('canUpdate') !== false || + this.rotateExisting.get('canUpdate') !== false || + this.rotateInternal.get('canUpdate') !== false + ); } - getHelpUrl(backend) { - return `/v1/${backend}/issuer/example?help=1`; + get canCrossSign() { + return this.crossSignPath.get('canUpdate') !== false; } - - @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; + get canSignIntermediate() { + return this.signIntermediate.get('canUpdate') !== false; + } + get canConfigure() { + return this.issuerPath.get('canUpdate') !== false; } } diff --git a/ui/app/serializers/pki/issuer.js b/ui/app/serializers/pki/issuer.js index 5db741e408538..6457e03687983 100644 --- a/ui/app/serializers/pki/issuer.js +++ b/ui/app/serializers/pki/issuer.js @@ -1,11 +1,28 @@ +import { parseCertificate } from 'vault/helpers/parse-pki-cert'; import ApplicationSerializer from '../application'; export default class PkiIssuerSerializer extends ApplicationSerializer { - // rehydrate each issuer model so all model attributes are accessible from the LIST response + primaryKey = 'issuer_id'; + + normalizeResponse(store, primaryModelClass, payload, id, requestType) { + if (payload.data.certificate) { + // Parse certificate back from the API and add to payload + const parsedCert = parseCertificate(payload.data.certificate); + const data = { issuer_ref: payload.issuer_id, ...payload.data, ...parsedCert }; + const json = super.normalizeResponse(store, primaryModelClass, { ...payload, data }, id, requestType); + return json; + } + return super.normalizeResponse(...arguments); + } + + // rehydrate each issuers 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] })); + return payload.data.keys.map((issuer_id) => ({ + issuer_id, + ...payload.data.key_info[issuer_id], + })); } Object.assign(payload, payload.data); delete payload.data; diff --git a/ui/lib/pki/addon/components/page/pki-issuer-details.hbs b/ui/lib/pki/addon/components/page/pki-issuer-details.hbs new file mode 100644 index 0000000000000..0b5505a5d2557 --- /dev/null +++ b/ui/lib/pki/addon/components/page/pki-issuer-details.hbs @@ -0,0 +1,77 @@ + + + {{#if @canRotate}} + + Rotate this root + + {{/if}} + {{#if @canCrossSign}} + + Cross-sign Issuer + + {{/if}} + {{#if @canSignIntermediate}} + + Sign Intermediate + + {{/if}} + + Download + + + {{#if @canConfigure}} + + Configure + + {{/if}} + + + +
+ {{#each @issuer.formFieldGroups as |fieldGroup|}} + {{#each-in fieldGroup as |group fields|}} +
+ {{#if (not-eq group "default")}} +

+ {{group}} +

+ {{/if}} + {{#each fields as |attr|}} + {{#if (eq attr.options.displayType "masked")}} + + + + {{else if (eq attr.name "keyId")}} + + {{get @issuer attr.name}} + + {{else}} + + {{/if}} + {{/each}} +
+ {{/each-in}} + {{/each}} + +
\ No newline at end of file diff --git a/ui/lib/pki/addon/routes/issuers/index.js b/ui/lib/pki/addon/routes/issuers/index.js index 5211ecc3f3d62..d895f8f374bc0 100644 --- a/ui/lib/pki/addon/routes/issuers/index.js +++ b/ui/lib/pki/addon/routes/issuers/index.js @@ -1,7 +1,7 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; -export default class PkiIssuersIndexRoute extends Route { +export default class PkiIssuersListRoute extends Route { @service store; @service secretMountPath; @service pathHelp; @@ -12,8 +12,6 @@ export default class PkiIssuersIndexRoute extends Route { } model() { - // the pathHelp service is needed for adding openAPI to the model - this.pathHelp.getNewModel('pki/issuer', 'pki'); return this.store .query('pki/issuer', { backend: this.secretMountPath.currentPath }) .then((issuersModel) => { diff --git a/ui/lib/pki/addon/routes/issuers/issuer/details.js b/ui/lib/pki/addon/routes/issuers/issuer/details.js index db8ba0c3ce9e6..ef04cad3fd38d 100644 --- a/ui/lib/pki/addon/routes/issuers/issuer/details.js +++ b/ui/lib/pki/addon/routes/issuers/issuer/details.js @@ -1,3 +1,9 @@ -import Route from '@ember/routing/route'; +import PkiIssuerIndexRoute from './index'; -export default class PkiIssuerDetailsRoute extends Route {} +export default class PkiIssuerDetailsRoute extends PkiIssuerIndexRoute { + // Details route gets issuer data from PkiIssuerIndexRoute + setupController(controller, resolvedModel) { + super.setupController(controller, resolvedModel); + controller.breadcrumbs.push({ label: resolvedModel.id }); + } +} diff --git a/ui/lib/pki/addon/routes/issuers/issuer/index.js b/ui/lib/pki/addon/routes/issuers/issuer/index.js new file mode 100644 index 0000000000000..e0548c91de2e2 --- /dev/null +++ b/ui/lib/pki/addon/routes/issuers/issuer/index.js @@ -0,0 +1,22 @@ +import PkiIssuersListRoute from '../index'; + +// Single issuer index route extends issuers list route +export default class PkiIssuerIndexRoute extends PkiIssuersListRoute { + model() { + const { issuer_ref } = this.paramsFor('issuers/issuer'); + return this.store.queryRecord('pki/issuer', { + backend: this.secretMountPath.currentPath, + id: issuer_ref, + }); + } + + setupController(controller, resolvedModel) { + super.setupController(controller, resolvedModel); + const backend = this.secretMountPath.currentPath || 'pki'; + controller.breadcrumbs = [ + { label: 'secrets', route: 'secrets', linkExternal: true }, + { label: backend, route: 'overview' }, + { label: 'issuers', route: 'issuers.index' }, + ]; + } +} diff --git a/ui/lib/pki/addon/templates/issuers/index.hbs b/ui/lib/pki/addon/templates/issuers/index.hbs index 099ed80f4ef0e..378d8099476d6 100644 --- a/ui/lib/pki/addon/templates/issuers/index.hbs +++ b/ui/lib/pki/addon/templates/issuers/index.hbs @@ -39,7 +39,11 @@ {{#if this.model.issuersModel.length}} {{#each this.model.issuersModel as |pkiIssuer|}} - +
@@ -63,12 +67,12 @@