mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 10:12:35 +00:00 
			
		
		
		
	UI: PKI Role toolbar (#18229)
This commit is contained in:
		| @@ -55,4 +55,8 @@ export default class PkiRoleAdapter extends ApplicationAdapter { | ||||
|   queryRecord(store, type, query) { | ||||
|     return this.fetchByQuery(store, query); | ||||
|   } | ||||
|   deleteRecord(store, type, snapshot) { | ||||
|     const { id, record } = snapshot; | ||||
|     return this.ajax(this._urlForRole(record.backend, id), 'DELETE'); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,71 @@ | ||||
| import Model, { attr } from '@ember-data/model'; | ||||
| import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; | ||||
| import { withModelValidations } from 'vault/decorators/model-validations'; | ||||
|  | ||||
| import fieldToAttrs from 'vault/utils/field-to-attrs'; | ||||
| import { withFormFields } from 'vault/decorators/model-form-fields'; | ||||
|  | ||||
| const validations = { | ||||
|   name: [{ type: 'presence', message: 'Name is required.' }], | ||||
| }; | ||||
|  | ||||
| const fieldGroups = [ | ||||
|   { | ||||
|     default: [ | ||||
|       'name', | ||||
|       'issuerRef', | ||||
|       'customTtl', | ||||
|       'notBeforeDuration', | ||||
|       'maxTtl', | ||||
|       'generateLease', | ||||
|       'noStore', | ||||
|       'addBasicConstraints', | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     'Domain handling': [ | ||||
|       'allowedDomains', | ||||
|       'allowedDomainsTemplate', | ||||
|       'allowBareDomains', | ||||
|       'allowSubdomains', | ||||
|       'allowGlobDomains', | ||||
|       'allowWildcardCertificates', | ||||
|       'allowLocalhost', // default: true (returned true by OpenApi) | ||||
|       'allowAnyName', | ||||
|       'enforceHostnames', // default: true (returned true by OpenApi) | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     'Key parameters': ['keyType', 'keyBits', 'signatureBits'], | ||||
|   }, | ||||
|   { | ||||
|     'Key usage': ['keyUsage', 'extKeyUsage', 'extKeyUsageOids'], | ||||
|   }, | ||||
|   { 'Policy identifiers': ['policyIdentifiers'] }, | ||||
|   { | ||||
|     'Subject Alternative Name (SAN) Options': [ | ||||
|       'allowIpSans', | ||||
|       'allowedUriSans', | ||||
|       'allowUriSansTemplate', | ||||
|       'allowedOtherSans', | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     'Additional subject fields': [ | ||||
|       'allowedSerialNumbers', | ||||
|       'requireCn', | ||||
|       'useCsrCommonName', | ||||
|       'useCsrSans', | ||||
|       'ou', | ||||
|       'organization', | ||||
|       'country', | ||||
|       'locality', | ||||
|       'province', | ||||
|       'streetAddress', | ||||
|       'postalCode', | ||||
|     ], | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @withFormFields(null, fieldGroups) | ||||
| @withModelValidations(validations) | ||||
| export default class PkiRoleModel extends Model { | ||||
|   get useOpenAPI() { | ||||
| @@ -242,34 +300,33 @@ export default class PkiRoleModel extends Model { | ||||
|   @attr({ hideFormSection: true }) postalCode; | ||||
|   /* End of overriding Additional subject field options */ | ||||
|  | ||||
|   /* CAPABILITIES */ | ||||
|   /* CAPABILITIES | ||||
|    * Default to show UI elements unless we know they can't access the given path | ||||
|    */ | ||||
|   @lazyCapabilities(apiPath`${'backend'}/roles/${'id'}`, 'backend', 'id') updatePath; | ||||
|   get canDelete() { | ||||
|     return this.updatePath.get('canCreate'); | ||||
|     return this.updatePath.get('isLoading') || this.updatePath.get('canCreate') !== false; | ||||
|   } | ||||
|   get canEdit() { | ||||
|     return this.updatePath.get('canEdit'); | ||||
|     return this.updatePath.get('isLoading') || this.updatePath.get('canUpdate') !== false; | ||||
|   } | ||||
|   get canRead() { | ||||
|     return this.updatePath.get('canRead'); | ||||
|     return this.updatePath.get('isLoading') || this.updatePath.get('canRead') !== false; | ||||
|   } | ||||
|  | ||||
|   @lazyCapabilities(apiPath`${'backend'}/issue/${'id'}`, 'backend', 'id') generatePath; | ||||
|   get canReadIssue() { | ||||
|     // ARG TODO was duplicate name, added Issue | ||||
|     return this.generatePath.get('canUpdate'); | ||||
|   get canGenerateCert() { | ||||
|     return this.generatePath.get('isLoading') || this.generatePath.get('canUpdate') !== false; | ||||
|   } | ||||
|   @lazyCapabilities(apiPath`${'backend'}/sign/${'id'}`, 'backend', 'id') signPath; | ||||
|   get canSign() { | ||||
|     return this.signPath.get('canUpdate'); | ||||
|     return this.signPath.get('isLoading') || this.signPath.get('canUpdate') !== false; | ||||
|   } | ||||
|   @lazyCapabilities(apiPath`${'backend'}/sign-verbatim/${'id'}`, 'backend', 'id') signVerbatimPath; | ||||
|   get canSignVerbatim() { | ||||
|     return this.signVerbatimPath.get('canUpdate'); | ||||
|     return this.signVerbatimPath.get('isLoading') || this.signVerbatimPath.get('canUpdate') !== false; | ||||
|   } | ||||
|  | ||||
|   _fieldToAttrsGroups = null; | ||||
|  | ||||
|   // Gets header/footer copy for specific toggle groups. | ||||
|   get fieldGroupsInfo() { | ||||
|     return { | ||||
| @@ -297,67 +354,4 @@ export default class PkiRoleModel extends Model { | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   get fieldGroups() { | ||||
|     if (!this._fieldToAttrsGroups) { | ||||
|       this._fieldToAttrsGroups = fieldToAttrs(this, [ | ||||
|         { | ||||
|           default: [ | ||||
|             'name', | ||||
|             'issuerRef', | ||||
|             'customTtl', | ||||
|             'notBeforeDuration', | ||||
|             'maxTtl', | ||||
|             'generateLease', | ||||
|             'noStore', | ||||
|             'addBasicConstraints', | ||||
|           ], | ||||
|         }, | ||||
|         { | ||||
|           'Domain handling': [ | ||||
|             'allowedDomains', | ||||
|             'allowedDomainsTemplate', | ||||
|             'allowBareDomains', | ||||
|             'allowSubdomains', | ||||
|             'allowGlobDomains', | ||||
|             'allowWildcardCertificates', | ||||
|             'allowLocalhost', // default: true (returned true by OpenApi) | ||||
|             'allowAnyName', | ||||
|             'enforceHostnames', // default: true (returned true by OpenApi) | ||||
|           ], | ||||
|         }, | ||||
|         { | ||||
|           'Key parameters': ['keyType', 'keyBits', 'signatureBits'], | ||||
|         }, | ||||
|         { | ||||
|           'Key usage': ['keyUsage', 'extKeyUsage', 'extKeyUsageOids'], | ||||
|         }, | ||||
|         { 'Policy identifiers': ['policyIdentifiers'] }, | ||||
|         { | ||||
|           'Subject Alternative Name (SAN) Options': [ | ||||
|             'allowIpSans', | ||||
|             'allowedUriSans', | ||||
|             'allowUriSansTemplate', | ||||
|             'allowedOtherSans', | ||||
|           ], | ||||
|         }, | ||||
|         { | ||||
|           'Additional subject fields': [ | ||||
|             'allowedSerialNumbers', | ||||
|             'requireCn', | ||||
|             'useCsrCommonName', | ||||
|             'useCsrSans', | ||||
|             'ou', | ||||
|             'organization', | ||||
|             'country', | ||||
|             'locality', | ||||
|             'province', | ||||
|             'streetAddress', | ||||
|             'postalCode', | ||||
|           ], | ||||
|         }, | ||||
|       ]); | ||||
|     } | ||||
|     return this._fieldToAttrsGroups; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -83,7 +83,7 @@ | ||||
|   </form> | ||||
| {{else}} | ||||
|   <div class="box is-sideless is-fullwidth is-marginless"> | ||||
|     {{#each this.model.fieldGroups as |fieldGroup|}} | ||||
|     {{#each this.model.formFieldGroups as |fieldGroup|}} | ||||
|       {{#each-in fieldGroup as |group fields|}} | ||||
|         {{#if (or (eq group "default") (eq group "Options"))}} | ||||
|           {{#each fields as |attr|}} | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| {{#unless this.dontShowTab}} | ||||
|   {{#if @isEngine}} | ||||
|     <LinkTo @route={{@link}}> | ||||
|     <LinkTo @route={{@link}} data-test-secret-list-tab={{@label}}> | ||||
|       {{@label}} | ||||
|     </LinkTo> | ||||
|   {{else}} | ||||
|   | ||||
| @@ -1,33 +1,38 @@ | ||||
| <PageHeader as |p|> | ||||
|   <p.top> | ||||
|     <Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} /> | ||||
|   </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> | ||||
|     {{#if @role.canDelete}} | ||||
|       <ConfirmAction | ||||
|         @buttonClasses="toolbar-link" | ||||
|         @onConfirmAction={{this.deleteRole}} | ||||
|         @confirmTitle="Delete role?" | ||||
|         @confirmButtonText="Delete" | ||||
|         data-test-pki-role-delete | ||||
|       > | ||||
|         Delete | ||||
|       </ConfirmAction> | ||||
|       <div class="toolbar-separator"></div> | ||||
|     {{/if}} | ||||
|     {{#if @role.canGenerateCert}} | ||||
|       <LinkTo class="toolbar-link" @route="roles.role.generate" @model={{@role.id}} data-test-pki-role-generate-cert> | ||||
|         Generate Certificate | ||||
|         <Icon @name="chevron-right" /> | ||||
|       </LinkTo> | ||||
|     {{/if}} | ||||
|     {{#if @role.canSign}} | ||||
|       <LinkTo class="toolbar-link" @route="roles.role.sign" @model={{@role.id}} data-test-pki-role-sign-cert> | ||||
|         Sign Certificate | ||||
|         <Icon @name="chevron-right" /> | ||||
|       </LinkTo> | ||||
|     {{/if}} | ||||
|     {{#if @role.canEdit}} | ||||
|       <LinkTo class="toolbar-link" @route="roles.role.edit" @model={{@role.id}} data-test-pki-role-edit-link> | ||||
|         Edit | ||||
|         <Icon @name="chevron-right" /> | ||||
|       </LinkTo> | ||||
|     {{/if}} | ||||
|   </ToolbarActions> | ||||
| </Toolbar> | ||||
| {{#each @role.fieldGroups as |fg|}} | ||||
| {{#each @role.formFieldGroups 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> | ||||
|   | ||||
| @@ -1,22 +1,24 @@ | ||||
| import { action } from '@ember/object'; | ||||
| import RouterService from '@ember/routing/router-service'; | ||||
| import Component from '@glimmer/component'; | ||||
|  | ||||
| // interface Attribute { | ||||
| //   name: string; | ||||
| //   options?: { | ||||
| //     label?: string; | ||||
| //   }; | ||||
| // } | ||||
| import FlashMessageService from 'vault/services/flash-messages'; | ||||
| import { inject as service } from '@ember/service'; | ||||
| import errorMessage from 'vault/utils/error-message'; | ||||
|  | ||||
| // TODO: pull this in from route model once it's TS | ||||
| interface Args { | ||||
|   role: { | ||||
|     backend: string; | ||||
|     id: string; | ||||
|     rollbackAttributes: () => void; | ||||
|     destroyRecord: () => void; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export default class DetailsPage extends Component<Args> { | ||||
|   @service declare readonly router: RouterService; | ||||
|   @service declare readonly flashMessages: FlashMessageService; | ||||
|  | ||||
|   get breadcrumbs() { | ||||
|     return [ | ||||
|       { label: 'secrets', route: 'secrets', linkExternal: true }, | ||||
| @@ -30,7 +32,15 @@ export default class DetailsPage extends Component<Args> { | ||||
|     return ['keyUsage', 'extKeyUsage', 'extKeyUsageOids']; | ||||
|   } | ||||
|  | ||||
|   @action deleteRole() { | ||||
|     // TODO: delete role | ||||
|   @action | ||||
|   async deleteRole() { | ||||
|     try { | ||||
|       await this.args.role.destroyRecord(); | ||||
|       this.flashMessages.success('Role deleted successfully'); | ||||
|       this.router.transitionTo('vault.cluster.secrets.backend.pki.roles.index'); | ||||
|     } catch (error) { | ||||
|       this.args.role.rollbackAttributes(); | ||||
|       this.flashMessages.danger(errorMessage(error)); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,25 +1,9 @@ | ||||
| <PageHeader as |p|> | ||||
|   <p.top> | ||||
|     <Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} /> | ||||
|   </p.top> | ||||
|   <p.levelLeft> | ||||
|     <h1 class="title is-3" data-test-role-details-title> | ||||
|       {{#if @model.isNew}} | ||||
|         Create a PKI role | ||||
|       {{else}} | ||||
|         Edit | ||||
|         {{@model.name}} | ||||
|       {{/if}} | ||||
|     </h1> | ||||
|   </p.levelLeft> | ||||
| </PageHeader> | ||||
|  | ||||
| <form {{on "submit" (perform this.save)}}> | ||||
|   <div class="box is-sideless is-fullwidth is-marginless"> | ||||
|     <MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" /> | ||||
|     {{! ARG TODO write a test for namespace reminder }} | ||||
|     <NamespaceReminder @mode={{if @model.isNew "create" "update"}} @noun="PKI role" /> | ||||
|     {{#each @model.fieldGroups as |fieldGroup|}} | ||||
|     {{#each @model.formFieldGroups as |fieldGroup|}} | ||||
|       {{#each-in fieldGroup as |group fields|}} | ||||
|         {{! DEFAULT VIEW }} | ||||
|         {{#if (eq group "default")}} | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export default class PkiCertificatesIndexRoute extends Route { | ||||
|  | ||||
|   beforeModel() { | ||||
|     // Must call this promise before the model hook otherwise it doesn't add OpenApi to record. | ||||
|     return this.pathHelp.getNewModel('pki/certificate', 'pki'); | ||||
|     return this.pathHelp.getNewModel('pki/certificate', this.secretMountPath.currentPath); | ||||
|   } | ||||
|  | ||||
|   model() { | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export default class PkiIssuersIndexRoute extends Route { | ||||
|  | ||||
|   beforeModel() { | ||||
|     // Must call this promise before the model hook otherwise it doesn't add OpenApi to record. | ||||
|     return this.pathHelp.getNewModel('pki/issuer', 'pki'); | ||||
|     return this.pathHelp.getNewModel('pki/issuer', this.secretMountPath.currentPath); | ||||
|   } | ||||
|  | ||||
|   model() { | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export default class PkiKeysIndexRoute extends Route { | ||||
|  | ||||
|   beforeModel() { | ||||
|     // Must call this promise before the model hook otherwise it doesn't add OpenApi to record. | ||||
|     return this.pathHelp.getNewModel('pki/key', 'pki'); | ||||
|     return this.pathHelp.getNewModel('pki/key', this.secretMountPath.currentPath); | ||||
|   } | ||||
|  | ||||
|   model() { | ||||
|   | ||||
| @@ -12,4 +12,15 @@ export default class PkiRolesCreateRoute extends PkiRolesIndexRoute { | ||||
|       backend: this.secretMountPath.currentPath, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   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: 'roles', route: 'roles.index' }, | ||||
|       { label: 'create' }, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,7 @@ export default class PkiRolesIndexRoute extends Route { | ||||
|   beforeModel() { | ||||
|     // Must call this promise before the model hook otherwise | ||||
|     // the model doesn't hydrate from OpenAPI correctly. | ||||
|     return this.pathHelp.getNewModel('pki/role', 'pki'); | ||||
|     return this.pathHelp.getNewModel('pki/role', this.secretMountPath.currentPath); | ||||
|   } | ||||
|  | ||||
|   model() { | ||||
|   | ||||
| @@ -8,4 +8,16 @@ export default class RolesRoleDetailsRoute extends PkiRolesIndexRoute { | ||||
|       id: role, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   setupController(controller, resolvedModel) { | ||||
|     super.setupController(controller, resolvedModel); | ||||
|     const { id } = resolvedModel; | ||||
|     const backend = this.secretMountPath.currentPath || 'pki'; | ||||
|     controller.breadcrumbs = [ | ||||
|       { label: 'secrets', route: 'secrets', linkExternal: true }, | ||||
|       { label: backend, route: 'overview' }, | ||||
|       { label: 'roles', route: 'roles.index' }, | ||||
|       { label: id }, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -10,4 +10,17 @@ export default class PkiRoleEditRoute extends PkiRolesIndexRoute { | ||||
|       id: role, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   setupController(controller, resolvedModel) { | ||||
|     super.setupController(controller, resolvedModel); | ||||
|     const { id } = resolvedModel; | ||||
|     const backend = this.secretMountPath.currentPath || 'pki'; | ||||
|     controller.breadcrumbs = [ | ||||
|       { label: 'secrets', route: 'secrets', linkExternal: true }, | ||||
|       { label: backend, route: 'overview' }, | ||||
|       { label: 'roles', route: 'roles.index' }, | ||||
|       { label: id, route: 'roles.role.details' }, | ||||
|       { label: 'edit' }, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,14 @@ | ||||
| <PageHeader as |p|> | ||||
|   <p.top> | ||||
|     <Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} /> | ||||
|   </p.top> | ||||
|   <p.levelLeft> | ||||
|     <h1 class="title is-3" data-test-pki-role-page-title> | ||||
|       Create a PKI role | ||||
|     </h1> | ||||
|   </p.levelLeft> | ||||
| </PageHeader> | ||||
|  | ||||
| <PkiRoleForm | ||||
|   @model={{this.model}} | ||||
|   @onCancel={{transition-to "vault.cluster.secrets.backend.pki.roles.index"}} | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
| /> | ||||
| <Toolbar> | ||||
|   <ToolbarActions> | ||||
|     <ToolbarLink @type="add" @route="roles.create"> | ||||
|     <ToolbarLink @type="add" @route="roles.create" data-test-pki-role-create-link> | ||||
|       Create role | ||||
|     </ToolbarLink> | ||||
|   </ToolbarActions> | ||||
|   | ||||
| @@ -1 +1,13 @@ | ||||
| <PageHeader as |p|> | ||||
|   <p.top> | ||||
|     <Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} /> | ||||
|   </p.top> | ||||
|   <p.levelLeft> | ||||
|     <h1 class="title is-3" data-test-pki-role-page-title> | ||||
|       <Icon @name="file-text" @size="24" class="has-text-grey-light" /> | ||||
|       PKI Role | ||||
|       <code>{{this.model.name}}</code> | ||||
|     </h1> | ||||
|   </p.levelLeft> | ||||
| </PageHeader> | ||||
| <Page::PkiRoleDetails @role={{this.model}} /> | ||||
| @@ -1,3 +1,13 @@ | ||||
| <PageHeader as |p|> | ||||
|   <p.top> | ||||
|     <Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} /> | ||||
|   </p.top> | ||||
|   <p.levelLeft> | ||||
|     <h1 class="title is-3" data-test-pki-role-page-title> | ||||
|       Edit role | ||||
|     </h1> | ||||
|   </p.levelLeft> | ||||
| </PageHeader> | ||||
| <PkiRoleForm | ||||
|   @model={{this.model}} | ||||
|   @onCancel={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.id}} | ||||
|   | ||||
							
								
								
									
										205
									
								
								ui/tests/acceptance/pki/pki-engine-workflow-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								ui/tests/acceptance/pki/pki-engine-workflow-test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,205 @@ | ||||
| import { create } from 'ember-cli-page-object'; | ||||
| import { module, test } from 'qunit'; | ||||
| import { setupApplicationTest } from 'ember-qunit'; | ||||
| import authPage from 'vault/tests/pages/auth'; | ||||
| import logout from 'vault/tests/pages/logout'; | ||||
| import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; | ||||
| import consoleClass from 'vault/tests/pages/components/console/ui-panel'; | ||||
| import { click, currentURL, fillIn, visit } from '@ember/test-helpers'; | ||||
| import { SELECTORS } from 'vault/tests/helpers/pki/workflow'; | ||||
|  | ||||
| const consoleComponent = create(consoleClass); | ||||
|  | ||||
| const tokenWithPolicy = async function (name, policy) { | ||||
|   await consoleComponent.runCommands([ | ||||
|     `write sys/policies/acl/${name} policy=${btoa(policy)}`, | ||||
|     `write -field=client_token auth/token/create policies=${name}`, | ||||
|   ]); | ||||
|   return consoleComponent.lastLogOutput; | ||||
| }; | ||||
|  | ||||
| const runCommands = async function (commands) { | ||||
|   try { | ||||
|     await consoleComponent.runCommands(commands); | ||||
|     const res = consoleComponent.lastLogOutput; | ||||
|     if (res.includes('Error')) { | ||||
|       throw new Error(res); | ||||
|     } | ||||
|     return res; | ||||
|   } catch (error) { | ||||
|     // eslint-disable-next-line no-console | ||||
|     console.error( | ||||
|       `The following occurred when trying to run the command(s):\n ${commands.join('\n')} \n\n ${ | ||||
|         consoleComponent.lastLogOutput | ||||
|       }` | ||||
|     ); | ||||
|     throw error; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * This test module should test the PKI workflow, including: | ||||
|  * - link between pages and confirm that the url is as expected | ||||
|  * - log in as user with a policy and ensure expected UI elements are shown/hidden | ||||
|  */ | ||||
| module('Acceptance | pki workflow', function (hooks) { | ||||
|   setupApplicationTest(hooks); | ||||
|  | ||||
|   hooks.beforeEach(async function () { | ||||
|     await authPage.login(); | ||||
|     // Setup PKI engine | ||||
|     const mountPath = `pki-workflow-${new Date().getTime()}`; | ||||
|     await enablePage.enable('pki', mountPath); | ||||
|     await runCommands([ | ||||
|       `write ${mountPath}/roles/some-role \ | ||||
|     issuer_ref="default" \ | ||||
|     allowed_domains="example.com" \ | ||||
|     allow_subdomains=true \ | ||||
|     max_ttl="720h"`, | ||||
|     ]); | ||||
|     this.mountPath = mountPath; | ||||
|  | ||||
|     await logout.visit(); | ||||
|   }); | ||||
|  | ||||
|   hooks.afterEach(async function () { | ||||
|     await logout.visit(); | ||||
|     await authPage.login(); | ||||
|     // Cleanup engine | ||||
|     await runCommands([`delete sys/mounts/${this.mountPath}`]); | ||||
|     await logout.visit(); | ||||
|   }); | ||||
|  | ||||
|   module('roles', function (hooks) { | ||||
|     hooks.beforeEach(async function () { | ||||
|       await authPage.login(); | ||||
|       // Setup role-specific items | ||||
|       await runCommands([ | ||||
|         `write ${this.mountPath}/roles/some-role \ | ||||
|       issuer_ref="default" \ | ||||
|       allowed_domains="example.com" \ | ||||
|       allow_subdomains=true \ | ||||
|       max_ttl="720h"`, | ||||
|       ]); | ||||
|       const pki_admin_policy = ` | ||||
|           path "${this.mountPath}/*" { | ||||
|             capabilities = ["create", "read", "update", "delete", "list"] | ||||
|           }, | ||||
|         `; | ||||
|       const pki_reader_policy = ` | ||||
|         path "${this.mountPath}/roles" { | ||||
|           capabilities = ["read", "list"] | ||||
|         }, | ||||
|         path "${this.mountPath}/roles/*" { | ||||
|           capabilities = ["read", "list"] | ||||
|         }, | ||||
|       `; | ||||
|       const pki_editor_policy = ` | ||||
|         path "${this.mountPath}/roles" { | ||||
|           capabilities = ["read", "list"] | ||||
|         }, | ||||
|         path "${this.mountPath}/roles/*" { | ||||
|           capabilities = ["read", "update"] | ||||
|         }, | ||||
|       `; | ||||
|       this.pkiRoleReader = await tokenWithPolicy('pki-reader', pki_reader_policy); | ||||
|       this.pkiRoleEditor = await tokenWithPolicy('pki-editor', pki_editor_policy); | ||||
|       this.pkiAdminToken = await tokenWithPolicy('pki-admin', pki_admin_policy); | ||||
|       await logout.visit(); | ||||
|     }); | ||||
|  | ||||
|     test('shows correct items if user has all permissions', async function (assert) { | ||||
|       await authPage.login(this.pkiAdminToken); | ||||
|       await visit(`/vault/secrets/${this.mountPath}/pki/overview`); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); | ||||
|       assert.dom(SELECTORS.rolesTab).exists('Roles tab is present'); | ||||
|       await click(SELECTORS.rolesTab); | ||||
|       assert.dom(SELECTORS.createRoleLink).exists({ count: 1 }, 'Create role link is rendered'); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`); | ||||
|       assert.dom('.linked-block').exists({ count: 1 }, 'One role is in list'); | ||||
|       await click('.linked-block'); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); | ||||
|  | ||||
|       assert.dom(SELECTORS.generateCertLink).exists('Generate cert link is shown'); | ||||
|       await click(SELECTORS.generateCertLink); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/generate`); | ||||
|  | ||||
|       // Go back to details and test all the links | ||||
|       await visit(`/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); | ||||
|       assert.dom(SELECTORS.signCertLink).exists('Sign cert link is shown'); | ||||
|       await click(SELECTORS.signCertLink); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/sign`); | ||||
|  | ||||
|       await visit(`/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); | ||||
|       assert.dom(SELECTORS.editRoleLink).exists('Edit link is shown'); | ||||
|       await click(SELECTORS.editRoleLink); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/edit`); | ||||
|  | ||||
|       await visit(`/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); | ||||
|       assert.dom(SELECTORS.deleteRoleButton).exists('Delete role button is shown'); | ||||
|       await click(`${SELECTORS.deleteRoleButton} [data-test-confirm-action-trigger]`); | ||||
|       await click(`[data-test-confirm-button]`); | ||||
|       assert.strictEqual( | ||||
|         currentURL(), | ||||
|         `/vault/secrets/${this.mountPath}/pki/roles`, | ||||
|         'redirects to roles list after deletion' | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     test('it does not show toolbar items the user does not have permission to see', async function (assert) { | ||||
|       await authPage.login(this.pkiRoleReader); | ||||
|       await visit(`/vault/secrets/${this.mountPath}/pki/overview`); | ||||
|       assert.dom(SELECTORS.rolesTab).exists('Roles tab is present'); | ||||
|       await click(SELECTORS.rolesTab); | ||||
|       assert.dom(SELECTORS.createRoleLink).exists({ count: 1 }, 'Create role link is rendered'); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`); | ||||
|       assert.dom('.linked-block').exists({ count: 1 }, 'One role is in list'); | ||||
|       await click('.linked-block'); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); | ||||
|       assert.dom(SELECTORS.deleteRoleButton).doesNotExist('Delete role button is not shown'); | ||||
|       assert.dom(SELECTORS.generateCertLink).doesNotExist('Generate cert link is not shown'); | ||||
|       assert.dom(SELECTORS.signCertLink).doesNotExist('Sign cert link is not shown'); | ||||
|       assert.dom(SELECTORS.editRoleLink).doesNotExist('Edit link is not shown'); | ||||
|     }); | ||||
|  | ||||
|     test('it shows correct toolbar items for the user policy', async function (assert) { | ||||
|       await authPage.login(this.pkiRoleEditor); | ||||
|       await visit(`/vault/secrets/${this.mountPath}/pki/overview`); | ||||
|       assert.dom(SELECTORS.rolesTab).exists('Roles tab is present'); | ||||
|       await click(SELECTORS.rolesTab); | ||||
|       assert.dom(SELECTORS.createRoleLink).exists({ count: 1 }, 'Create role link is rendered'); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`); | ||||
|       assert.dom('.linked-block').exists({ count: 1 }, 'One role is in list'); | ||||
|       await click('.linked-block'); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/details`); | ||||
|       assert.dom(SELECTORS.deleteRoleButton).doesNotExist('Delete role button is not shown'); | ||||
|       assert.dom(SELECTORS.generateCertLink).doesNotExist('Generate cert link is not shown'); | ||||
|       assert.dom(SELECTORS.signCertLink).doesNotExist('Sign cert link is not shown'); | ||||
|       assert.dom(SELECTORS.editRoleLink).exists('Edit link is shown'); | ||||
|       await click(SELECTORS.editRoleLink); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/edit`); | ||||
|     }); | ||||
|  | ||||
|     test('create role happy path', async function (assert) { | ||||
|       const roleName = 'another-role'; | ||||
|       await authPage.login(this.pkiAdminToken); | ||||
|       await visit(`/vault/secrets/${this.mountPath}/pki/overview`); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`); | ||||
|       assert.dom(SELECTORS.rolesTab).exists('Roles tab is present'); | ||||
|       await click(SELECTORS.rolesTab); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`); | ||||
|       await click(SELECTORS.createRoleLink); | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/create`); | ||||
|       assert.dom(SELECTORS.breadcrumbContainer).exists({ count: 1 }, 'breadcrumbs are rendered'); | ||||
|       assert.dom(SELECTORS.breadcrumbs).exists({ count: 4 }, 'Shows 4 breadcrumbs'); | ||||
|       assert.dom(SELECTORS.pageTitle).hasText('Create a PKI role'); | ||||
|  | ||||
|       await fillIn(SELECTORS.roleForm.roleName, roleName); | ||||
|       await click(SELECTORS.roleForm.roleCreateButton); | ||||
|  | ||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/${roleName}/details`); | ||||
|       assert.dom(SELECTORS.breadcrumbs).exists({ count: 4 }, 'Shows 4 breadcrumbs'); | ||||
|       assert.dom(SELECTORS.pageTitle).hasText(`PKI Role ${roleName}`); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,7 +1,4 @@ | ||||
| export const SELECTORS = { | ||||
|   breadcrumbContainer: '[data-test-breadcrumbs]', | ||||
|   breadcrumbs: '[data-test-breadcrumbs] 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"]', | ||||
|   | ||||
							
								
								
									
										22
									
								
								ui/tests/helpers/pki/workflow.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								ui/tests/helpers/pki/workflow.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import { SELECTORS as ROLEFORM } from './roles/form'; | ||||
| export const SELECTORS = { | ||||
|   breadcrumbContainer: '[data-test-breadcrumbs]', | ||||
|   breadcrumbs: '[data-test-breadcrumbs] li', | ||||
|   pageTitle: '[data-test-pki-role-page-title]', | ||||
|   // TABS | ||||
|   overviewTab: '[data-test-secret-list-tab="Overview"]', | ||||
|   rolesTab: '[data-test-secret-list-tab="Roles"]', | ||||
|   issuersTab: '[data-test-secret-list-tab="Issuers"]', | ||||
|   certsTab: '[data-test-secret-list-tab="Certificates"]', | ||||
|   keysTab: '[data-test-secret-list-tab="Keys"]', | ||||
|   configTab: '[data-test-secret-list-tab="Configuration"]', | ||||
|   // ROLES | ||||
|   deleteRoleButton: '[data-test-pki-role-delete]', | ||||
|   generateCertLink: '[data-test-pki-role-generate-cert]', | ||||
|   signCertLink: '[data-test-pki-role-sign-cert]', | ||||
|   editRoleLink: '[data-test-pki-role-edit-link]', | ||||
|   createRoleLink: '[data-test-pki-role-create-link]', | ||||
|   roleForm: { | ||||
|     ...ROLEFORM, | ||||
|   }, | ||||
| }; | ||||
| @@ -27,7 +27,7 @@ module('Integration | Component | pki key details page', function (hooks) { | ||||
|   }); | ||||
|  | ||||
|   test('it renders the page component and deletes a key', async function (assert) { | ||||
|     assert.expect(9); | ||||
|     assert.expect(7); | ||||
|     this.server.delete(`${this.backend}/key/${this.model.keyId}`, () => { | ||||
|       assert.ok(true, 'confirming delete fires off destroyRecord()'); | ||||
|     }); | ||||
| @@ -39,8 +39,6 @@ module('Integration | Component | pki key details page', function (hooks) { | ||||
|       { 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('View key', 'title renders'); | ||||
|     assert.dom(SELECTORS.keyIdValue).hasText(' 724862ff-6438-bad0-b598-77a6c7f4e934', 'key id renders'); | ||||
|     assert.dom(SELECTORS.keyNameValue).hasText('test-key', 'key name renders'); | ||||
|   | ||||
| @@ -11,9 +11,8 @@ module('Integration | Component | pki-key-parameters', function (hooks) { | ||||
|  | ||||
|   hooks.beforeEach(function () { | ||||
|     this.store = this.owner.lookup('service:store'); | ||||
|     this.model = this.store.createRecord('pki/role'); | ||||
|     this.model.backend = 'pki'; | ||||
|     [this.fields] = Object.values(this.model.fieldGroups.find((g) => g['Key parameters'])); | ||||
|     this.model = this.store.createRecord('pki/role', { backend: 'pki' }); | ||||
|     [this.fields] = Object.values(this.model.formFieldGroups.find((g) => g['Key parameters'])); | ||||
|   }); | ||||
|  | ||||
|   test('it should render the component and display the correct defaults', async function (assert) { | ||||
|   | ||||
| @@ -22,17 +22,13 @@ module('Integration | Component | pki role details page', function (hooks) { | ||||
|   }); | ||||
|  | ||||
|   test('it should render the page component', async function (assert) { | ||||
|     assert.expect(8); | ||||
|     assert.expect(5); | ||||
|     await render( | ||||
|       hbs` | ||||
|       <Page::PkiRoleDetails @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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Chelsea Shaw
					Chelsea Shaw