mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
Merge remote-tracking branch 'remotes/from/ce/main'
Some checks are pending
build / setup (push) Waiting to run
build / Check ce/* Pull Requests (push) Blocked by required conditions
build / ui (push) Blocked by required conditions
build / artifacts-ce (push) Blocked by required conditions
build / artifacts-ent (push) Blocked by required conditions
build / hcp-image (push) Blocked by required conditions
build / test (push) Blocked by required conditions
build / test-hcp-image (push) Blocked by required conditions
build / completed-successfully (push) Blocked by required conditions
CI / setup (push) Waiting to run
CI / Run Autopilot upgrade tool (push) Blocked by required conditions
CI / Run Go tests (push) Blocked by required conditions
CI / Run Go tests tagged with testonly (push) Blocked by required conditions
CI / Run Go tests with data race detection (push) Blocked by required conditions
CI / Run Go tests with FIPS configuration (push) Blocked by required conditions
CI / Test UI (push) Blocked by required conditions
CI / tests-completed (push) Blocked by required conditions
Run linters / Setup (push) Waiting to run
Run linters / Deprecated functions (push) Blocked by required conditions
Run linters / Code checks (push) Blocked by required conditions
Run linters / Protobuf generate delta (push) Blocked by required conditions
Run linters / Format (push) Blocked by required conditions
Run linters / Semgrep (push) Waiting to run
Check Copywrite Headers / copywrite (push) Waiting to run
Security Scan / scan (push) Waiting to run
Some checks are pending
build / setup (push) Waiting to run
build / Check ce/* Pull Requests (push) Blocked by required conditions
build / ui (push) Blocked by required conditions
build / artifacts-ce (push) Blocked by required conditions
build / artifacts-ent (push) Blocked by required conditions
build / hcp-image (push) Blocked by required conditions
build / test (push) Blocked by required conditions
build / test-hcp-image (push) Blocked by required conditions
build / completed-successfully (push) Blocked by required conditions
CI / setup (push) Waiting to run
CI / Run Autopilot upgrade tool (push) Blocked by required conditions
CI / Run Go tests (push) Blocked by required conditions
CI / Run Go tests tagged with testonly (push) Blocked by required conditions
CI / Run Go tests with data race detection (push) Blocked by required conditions
CI / Run Go tests with FIPS configuration (push) Blocked by required conditions
CI / Test UI (push) Blocked by required conditions
CI / tests-completed (push) Blocked by required conditions
Run linters / Setup (push) Waiting to run
Run linters / Deprecated functions (push) Blocked by required conditions
Run linters / Code checks (push) Blocked by required conditions
Run linters / Protobuf generate delta (push) Blocked by required conditions
Run linters / Format (push) Blocked by required conditions
Run linters / Semgrep (push) Waiting to run
Check Copywrite Headers / copywrite (push) Waiting to run
Security Scan / scan (push) Waiting to run
This commit is contained in:
commit
2c9789df94
15 changed files with 289 additions and 125 deletions
|
|
@ -1,3 +1,3 @@
|
|||
```release-note:feature
|
||||
**UI Policy Generator (Enterprise)**: Adds policy generator flyout to KV V2 secrets engine prepopulated with relevant API requests for each page.
|
||||
**UI Policy Generator (Enterprise)**: Adds policy generator flyout to KV V2 and PKI secrets engines prepopulated with relevant API requests for each page.
|
||||
```
|
||||
|
|
@ -16,51 +16,56 @@ export const SUDO_PATHS = [
|
|||
export const SUDO_PATH_PREFIXES = ['sys/leases/revoke-prefix', 'sys/leases/revoke-force'];
|
||||
|
||||
export const PATH_MAP = {
|
||||
customLogin: apiPath`sys/config/ui/login/default-auth/${'id'}`,
|
||||
customMessages: apiPath`sys/config/ui/custom-messages/${'id'}`,
|
||||
syncActivate: apiPath`sys/activation-flags/secrets-sync/activate`,
|
||||
syncDestination: apiPath`sys/sync/destinations/${'type'}/${'name'}`,
|
||||
syncSetAssociation: apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/set`,
|
||||
syncRemoveAssociation: apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/remove`,
|
||||
kvConfig: apiPath`${'path'}/config`,
|
||||
kvMetadata: apiPath`${'backend'}/metadata/${'path'}`,
|
||||
authMethodConfig: apiPath`auth/${'path'}/config`,
|
||||
authMethodConfigAws: apiPath`auth/${'path'}/config/client`,
|
||||
authMethodDelete: apiPath`sys/auth/${'path'}`,
|
||||
pkiRevoke: apiPath`${'backend'}/revoke`,
|
||||
clientsActivityExport: apiPath`${'namespace'}/sys/internal/counters/activity/export`,
|
||||
clientsConfig: apiPath`sys/internal/counters/config`,
|
||||
customLogin: apiPath`sys/config/ui/login/default-auth/${'id'}`,
|
||||
customMessages: apiPath`sys/config/ui/custom-messages/${'id'}`,
|
||||
kmipCredentialsRevoke: apiPath`${'backend'}/scope/${'scope'}/role/${'role'}/credentials/revoke`,
|
||||
kmipRole: apiPath`${'backend'}/scopes/${'scope'}/roles/${'name'}`,
|
||||
kmipScope: apiPath`${'backend'}/scopes/${'name'}`,
|
||||
kubernetesCreds: apiPath`${'backend'}/creds/${'name'}`,
|
||||
kubernetesRole: apiPath`${'backend'}/role/${'name'}`,
|
||||
kvConfig: apiPath`${'path'}/config`,
|
||||
kvMetadata: apiPath`${'backend'}/metadata/${'path'}`,
|
||||
ldapDynamicRole: apiPath`${'backend'}/role/${'name'}`,
|
||||
ldapDynamicRoleCreds: apiPath`${'backend'}/creds/${'name'}`,
|
||||
ldapLibrary: apiPath`${'backend'}/library/${'name'}`,
|
||||
ldapLibraryCheckIn: apiPath`${'backend'}/library/${'name'}/check-in`,
|
||||
ldapLibraryCheckOut: apiPath`${'backend'}/library/${'name'}/check-out`,
|
||||
ldapRotateStaticRole: apiPath`${'backend'}/rotate-role/${'name'}`,
|
||||
ldapStaticRole: apiPath`${'backend'}/static-role/${'name'}`,
|
||||
ldapStaticRoleCreds: apiPath`${'backend'}/static-cred/${'name'}`,
|
||||
pkiCertificates: apiPath`${'backend'}/certificates`,
|
||||
pkiConfigAcme: apiPath`${'backend'}/config/acme`,
|
||||
pkiConfigAutoTidy: apiPath`${'backend'}/config/auto-tidy`,
|
||||
pkiConfigCluster: apiPath`${'backend'}/config/cluster`,
|
||||
pkiConfigCrl: apiPath`${'backend'}/config/crl`,
|
||||
pkiConfigUrls: apiPath`${'backend'}/config/urls`,
|
||||
pkiIssuersImportBundle: apiPath`${'backend'}/issuers/import/bundle`,
|
||||
pkiIssuersGenerateRoot: apiPath`${'backend'}/issuers/generate/root/${'type'}`,
|
||||
pkiIssuersGenerateIntermediate: apiPath`${'backend'}/issuers/generate/intermediate/${'type'}`,
|
||||
pkiIssuersCrossSign: apiPath`${'backend'}/issuers/cross-sign`,
|
||||
pkiIssuer: apiPath`${'backend'}/issuer/${'issuerId'}`,
|
||||
pkiIssuerSignIntermediate: apiPath`${'backend'}/issuer/${'issuerId'}/sign-intermediate`,
|
||||
pkiRoot: apiPath`${'backend'}/root`,
|
||||
pkiRootRotate: apiPath`${'backend'}/root/rotate/${'type'}`,
|
||||
pkiIntermediateCrossSign: apiPath`${'backend'}/intermediate/cross-sign`,
|
||||
pkiIssue: apiPath`${'backend'}/issue/${'id'}`,
|
||||
pkiIssuer: apiPath`${'backend'}/issuer/${'issuerId'}`,
|
||||
pkiIssuersCrossSign: apiPath`${'backend'}/issuers/cross-sign`,
|
||||
pkiIssuersGenerateIntermediate: apiPath`${'backend'}/issuers/generate/intermediate/${'type'}`,
|
||||
pkiIssuersGenerateRoot: apiPath`${'backend'}/issuers/generate/root/${'type'}`,
|
||||
pkiIssuerSignIntermediate: apiPath`${'backend'}/issuer/${'issuerId'}/sign-intermediate`,
|
||||
pkiIssuersImportBundle: apiPath`${'backend'}/issuers/import/bundle`,
|
||||
pkiKey: apiPath`${'backend'}/key/${'keyId'}`,
|
||||
pkiKeysGenerate: apiPath`${'backend'}/keys/generate`,
|
||||
pkiKeysImport: apiPath`${'backend'}/keys/import`,
|
||||
pkiRevoke: apiPath`${'backend'}/revoke`,
|
||||
pkiRole: apiPath`${'backend'}/roles/${'id'}`,
|
||||
pkiIssue: apiPath`${'backend'}/issue/${'id'}`,
|
||||
pkiRoles: apiPath`${'backend'}/roles`,
|
||||
pkiRoot: apiPath`${'backend'}/root`,
|
||||
pkiRootRotate: apiPath`${'backend'}/root/rotate/${'type'}`,
|
||||
pkiSign: apiPath`${'backend'}/sign/${'id'}`,
|
||||
pkiSignVerbatim: apiPath`${'backend'}/sign-verbatim/${'id'}`,
|
||||
ldapStaticRole: apiPath`${'backend'}/static-role/${'name'}`,
|
||||
ldapDynamicRole: apiPath`${'backend'}/role/${'name'}`,
|
||||
ldapRotateStaticRole: apiPath`${'backend'}/rotate-role/${'name'}`,
|
||||
ldapStaticRoleCreds: apiPath`${'backend'}/static-cred/${'name'}`,
|
||||
ldapDynamicRoleCreds: apiPath`${'backend'}/creds/${'name'}`,
|
||||
ldapLibrary: apiPath`${'backend'}/library/${'name'}`,
|
||||
ldapLibraryCheckOut: apiPath`${'backend'}/library/${'name'}/check-out`,
|
||||
ldapLibraryCheckIn: apiPath`${'backend'}/library/${'name'}/check-in`,
|
||||
kubernetesRole: apiPath`${'backend'}/role/${'name'}`,
|
||||
kubernetesCreds: apiPath`${'backend'}/creds/${'name'}`,
|
||||
kmipScope: apiPath`${'backend'}/scopes/${'name'}`,
|
||||
kmipRole: apiPath`${'backend'}/scopes/${'scope'}/roles/${'name'}`,
|
||||
kmipCredentialsRevoke: apiPath`${'backend'}/scope/${'scope'}/role/${'role'}/credentials/revoke`,
|
||||
clientsConfig: apiPath`sys/internal/counters/config`,
|
||||
clientsActivityExport: apiPath`${'namespace'}/sys/internal/counters/activity/export`,
|
||||
pkiTidy: apiPath`${'backend'}/tidy`,
|
||||
pkiTidyStatus: apiPath`${'backend'}/tidy/status`,
|
||||
syncActivate: apiPath`sys/activation-flags/secrets-sync/activate`,
|
||||
syncDestination: apiPath`sys/sync/destinations/${'type'}/${'name'}`,
|
||||
syncRemoveAssociation: apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/remove`,
|
||||
syncSetAssociation: apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/set`,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
@type="text"
|
||||
@id={{@id}}
|
||||
@value={{this.searchInput}}
|
||||
{{on "keyup" this.inputChanged}}
|
||||
{{on (or @changeEvent "keyup") this.inputChanged}}
|
||||
placeholder={{@placeholder}}
|
||||
autocomplete="off"
|
||||
data-test-input-search={{@id}}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,14 @@ import { tracked } from '@glimmer/tracking';
|
|||
|
||||
/**
|
||||
* @module InputSearch
|
||||
* This component renders an input that fires a callback on "keyup" containing the input's value
|
||||
* This component renders an input that fires a callback on "keyup" or the passed change event containing the input's value
|
||||
*
|
||||
* @example
|
||||
* <InputSearch @initialValue="secret/path/" @onChange={{this.handleSearch}} @placeholder="search..." />
|
||||
*
|
||||
* @param {string} [id] - unique id for the input
|
||||
* @param {string} [initialValue] - initial search value, i.e. a secret path prefix, that pre-fills the input field
|
||||
* @param {string} [changeEvent="keyup"] - the input change event for which to fire the onChange callback
|
||||
* @param {string} [placeholder] - placeholder text for the input
|
||||
* @param {string} [label] - label for the input
|
||||
* @param {string} [subtext] - displays below the label
|
||||
|
|
|
|||
|
|
@ -44,34 +44,36 @@
|
|||
</div>
|
||||
<div class="level-right is-flex is-paddingless is-marginless">
|
||||
<div class="level-item">
|
||||
{{#if (or @canRead @canEdit)}}
|
||||
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="Manage key"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
{{#if @canRead}}
|
||||
<dd.Interactive
|
||||
@route="keys.key.details"
|
||||
@models={{array @backend pkiKey.key_id}}
|
||||
data-test-key-menu-link="details"
|
||||
>
|
||||
Details
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if @canEdit}}
|
||||
<dd.Interactive
|
||||
@route="keys.key.edit"
|
||||
@models={{array @backend pkiKey.key_id}}
|
||||
data-test-key-menu-link="edit"
|
||||
>
|
||||
Edit
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
{{/if}}
|
||||
{{#let (get @keyPermsById pkiKey.key_id) as |perms|}}
|
||||
{{#if (or perms.canRead perms.canUpdate)}}
|
||||
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="Manage key"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
{{#if perms.canRead}}
|
||||
<dd.Interactive
|
||||
@route="keys.key.details"
|
||||
@models={{array @backend pkiKey.key_id}}
|
||||
data-test-key-menu-link="details"
|
||||
>
|
||||
Details
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if perms.canUpdate}}
|
||||
<dd.Interactive
|
||||
@route="keys.key.edit"
|
||||
@models={{array @backend pkiKey.key_id}}
|
||||
data-test-key-menu-link="edit"
|
||||
>
|
||||
Edit
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@
|
|||
</:action>
|
||||
<:content>
|
||||
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
|
||||
{{format-number (if (eq @issuers 404) 0 @issuers.length)}}
|
||||
{{format-number @issuers.length}}
|
||||
</Hds::Text::Display>
|
||||
</:content>
|
||||
</OverviewCard>
|
||||
{{#if (not-eq @roles 403)}}
|
||||
{{#if @canListRoles}}
|
||||
<OverviewCard
|
||||
@cardTitle="Roles"
|
||||
@subText="The total number of roles in this PKI mount that have been created to generate certificates."
|
||||
|
|
@ -39,24 +39,37 @@
|
|||
</:action>
|
||||
<:content>
|
||||
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
|
||||
{{format-number (if (eq @roles 404) 0 @roles.length)}}
|
||||
{{format-number @roles.length}}
|
||||
</Hds::Text::Display>
|
||||
</:content>
|
||||
</OverviewCard>
|
||||
{{/if}}
|
||||
<OverviewCard @cardTitle="Issue certificate" @subText="Begin issuing a certificate by choosing a role.">
|
||||
<OverviewCard
|
||||
@cardTitle="Issue certificate"
|
||||
@subText="Begin issuing a certificate by {{if @canListRoles 'choosing' 'entering'}} a role."
|
||||
>
|
||||
<:content>
|
||||
<div class="has-top-margin-m is-flex">
|
||||
<SearchSelect
|
||||
class="is-flex-grow-1"
|
||||
@ariaLabel="Role"
|
||||
@selectLimit="1"
|
||||
@options={{this.searchSelectOptions.roles}}
|
||||
@placeholder="Type to find a role..."
|
||||
@disallowNewItems={{true}}
|
||||
@onChange={{this.handleRolesInput}}
|
||||
data-test-issue-certificate-input
|
||||
/>
|
||||
{{#if @canListRoles}}
|
||||
<SearchSelect
|
||||
class="is-flex-grow-1"
|
||||
@ariaLabel="Role"
|
||||
@selectLimit="1"
|
||||
@options={{this.searchSelectOptions.roles}}
|
||||
@placeholder="Type to find a role..."
|
||||
@disallowNewItems={{true}}
|
||||
@onChange={{this.handleRolesInput}}
|
||||
data-test-issue-certificate-input
|
||||
/>
|
||||
{{else}}
|
||||
<InputSearch
|
||||
class="is-flex-grow-1"
|
||||
@placeholder="Input a role.."
|
||||
@onChange={{this.handleRolesInput}}
|
||||
@changeEvent="input"
|
||||
@id="role"
|
||||
/>
|
||||
{{/if}}
|
||||
<Hds::Button
|
||||
@text="Issue"
|
||||
@color="secondary"
|
||||
|
|
@ -70,19 +83,32 @@
|
|||
</:content>
|
||||
</OverviewCard>
|
||||
|
||||
<OverviewCard @cardTitle="View certificate" @subText="Quickly view a certificate by typing its serial number.">
|
||||
<OverviewCard
|
||||
@cardTitle="View certificate"
|
||||
@subText="Quickly view a certificate by {{if @canListCertificates 'looking up' 'providing'}} its serial number."
|
||||
>
|
||||
<:content>
|
||||
<div class="has-top-margin-m {{unless this.certificateValue 'is-flex'}}">
|
||||
<SearchSelect
|
||||
class="is-flex-grow-1"
|
||||
@ariaLabel="Certificate serial number"
|
||||
@selectLimit="1"
|
||||
@options={{this.searchSelectOptions.certificates}}
|
||||
@placeholder="33:a3:..."
|
||||
@disallowNewItems={{true}}
|
||||
@onChange={{this.handleCertificateInput}}
|
||||
data-test-view-certificate-input
|
||||
/>
|
||||
{{#if @canListCertificates}}
|
||||
<SearchSelect
|
||||
class="is-flex-grow-1"
|
||||
@ariaLabel="Certificate serial number"
|
||||
@selectLimit="1"
|
||||
@options={{this.searchSelectOptions.certificates}}
|
||||
@placeholder="33:a3:..."
|
||||
@disallowNewItems={{true}}
|
||||
@onChange={{this.handleCertificateInput}}
|
||||
data-test-view-certificate-input
|
||||
/>
|
||||
{{else}}
|
||||
<InputSearch
|
||||
class="is-flex-grow-1"
|
||||
@placeholder="33:a3:..."
|
||||
@onChange={{this.handleCertificateInput}}
|
||||
@changeEvent="input"
|
||||
@id="certificate"
|
||||
/>
|
||||
{{/if}}
|
||||
<Hds::Button
|
||||
@text="View"
|
||||
@color="secondary"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
</:breadcrumbs>
|
||||
|
||||
<:actions>
|
||||
<CodeGenerator::Policy::Flyout @policyPaths={{this.policyPaths}} />
|
||||
{{#if @configRoute}}
|
||||
<Hds::Button @color="secondary" @route="overview" @text="Exit configuration" data-test-button="Exit configuration" />
|
||||
{{else}}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import { tracked } from '@glimmer/tracking';
|
|||
import Component from '@glimmer/component';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type { PATH_MAP } from 'vault/utils/constants/capabilities';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type CapabilitiesService from 'vault/services/capabilities';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
|
||||
/**
|
||||
* @module PkiPageHeader
|
||||
|
|
@ -26,22 +28,40 @@ interface Args {
|
|||
backend: { id: string };
|
||||
}
|
||||
|
||||
const ROUTE_PATH_MAP = {
|
||||
'vault.cluster.secrets.backend.pki.certificates.index': ['pkiCertificates'],
|
||||
'vault.cluster.secrets.backend.pki.roles.index': ['pkiRoles'],
|
||||
'vault.cluster.secrets.backend.pki.tidy.index': ['pkiTidy', 'pkiTidyStatus', 'pkiConfigAutoTidy'],
|
||||
} satisfies Record<string, readonly (keyof typeof PATH_MAP)[]>;
|
||||
|
||||
export default class PkiPageHeader extends Component<Args> {
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly capabilities: CapabilitiesService;
|
||||
|
||||
@tracked engineToDisable = undefined;
|
||||
|
||||
get breadcrumbs() {
|
||||
return [
|
||||
{ label: 'Vault', route: 'vault', icon: 'vault', linkExternal: true },
|
||||
{ label: 'Secrets engines', route: 'secrets', linkExternal: true },
|
||||
{
|
||||
label: this.args?.backend?.id,
|
||||
},
|
||||
{ label: this.args?.backend?.id },
|
||||
];
|
||||
}
|
||||
|
||||
// PKI does not make capability requests for these routes
|
||||
// so manually pass the relevant paths for each route.
|
||||
get policyPaths() {
|
||||
const backend = this.args?.backend?.id;
|
||||
const { currentRouteName } = this.router;
|
||||
const paths = ROUTE_PATH_MAP[currentRouteName as keyof typeof ROUTE_PATH_MAP];
|
||||
if (paths) {
|
||||
return this.capabilities.pathsForList(paths, { backend });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@task
|
||||
*disableEngine(engine: SecretsEngineResource) {
|
||||
const { engineType, id, path } = engine;
|
||||
|
|
|
|||
|
|
@ -22,21 +22,24 @@ export default class PkiKeysIndexRoute extends Route {
|
|||
},
|
||||
};
|
||||
|
||||
async fetchCapabilities(keyId) {
|
||||
async fetchCapabilities(keys) {
|
||||
const { pathFor } = this.capabilities;
|
||||
const backend = this.secretMountPath.currentPath;
|
||||
const keyPathsById = this.keyPathsById(backend, keys);
|
||||
const pathMap = {
|
||||
import: pathFor('pkiKeysImport', { backend }),
|
||||
generate: pathFor('pkiKeysGenerate', { backend }),
|
||||
key: pathFor('pkiKey', { backend, keyId }),
|
||||
...keyPathsById,
|
||||
};
|
||||
const perms = await this.capabilities.fetch(Object.values(pathMap));
|
||||
|
||||
const apiPaths = Object.values(pathMap);
|
||||
const perms = await this.capabilities.fetch(apiPaths, {
|
||||
routeForCache: 'vault.cluster.secrets.backend.pki.keys',
|
||||
});
|
||||
return {
|
||||
canImportKeys: perms[pathMap.import].canUpdate,
|
||||
canGenerateKeys: perms[pathMap.generate].canUpdate,
|
||||
canRead: perms[pathMap.key].canRead,
|
||||
canEdit: perms[pathMap.key].canUpdate,
|
||||
keyPermsById: this.keyCapabilitiesById(keyPathsById, perms),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +56,7 @@ export default class PkiKeysIndexRoute extends Route {
|
|||
PkiListKeysListEnum.TRUE
|
||||
);
|
||||
const keys = this.api.keyInfoToArray(response, 'key_id');
|
||||
const capabilities = await this.fetchCapabilities(keys[0].key_id);
|
||||
const capabilities = await this.fetchCapabilities(keys);
|
||||
Object.assign(model, { ...capabilities, keys: paginate(keys, { page }) });
|
||||
} catch (e) {
|
||||
if (e.response.status === 404) {
|
||||
|
|
@ -82,4 +85,20 @@ export default class PkiKeysIndexRoute extends Route {
|
|||
controller.set('page', undefined);
|
||||
}
|
||||
}
|
||||
|
||||
keyPathsById(backend, keys) {
|
||||
// Construct API path for each key in the list
|
||||
return Object.fromEntries(
|
||||
keys.map(({ key_id: keyId }) => [keyId, this.capabilities.pathFor('pkiKey', { backend, keyId })])
|
||||
);
|
||||
}
|
||||
|
||||
keyCapabilitiesById(keyPathsById, perms) {
|
||||
// Iterate over key ids and return an object with Capabilities as their value
|
||||
return Object.fromEntries(
|
||||
Object.entries(keyPathsById)
|
||||
.filter(([, apiPath]) => apiPath in perms)
|
||||
.map(([keyId, apiPath]) => [keyId, perms[apiPath]])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ export const getCliMessage = (msg) => {
|
|||
|
||||
@withConfig()
|
||||
export default class PkiOverviewRoute extends Route {
|
||||
@service secretMountPath;
|
||||
@service auth;
|
||||
@service api;
|
||||
@service capabilities;
|
||||
@service secretMountPath;
|
||||
|
||||
async fetchAllCertificates() {
|
||||
try {
|
||||
|
|
@ -36,7 +36,10 @@ export default class PkiOverviewRoute extends Route {
|
|||
);
|
||||
return keys;
|
||||
} catch (e) {
|
||||
return e.response.status;
|
||||
const { status } = await this.api.parseError(e);
|
||||
// If there was a permissions (403) or some other error
|
||||
// swallow because this data is for rendering overview cards
|
||||
return status === 404 ? [] : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +51,10 @@ export default class PkiOverviewRoute extends Route {
|
|||
);
|
||||
return keys;
|
||||
} catch (e) {
|
||||
return e.response.status;
|
||||
const { status } = await this.api.parseError(e);
|
||||
// If there was a permissions (403) or some other error
|
||||
// swallow because this data is for rendering overview cards
|
||||
return status === 404 ? [] : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,17 +66,39 @@ export default class PkiOverviewRoute extends Route {
|
|||
);
|
||||
return keys;
|
||||
} catch (e) {
|
||||
return e.response.status;
|
||||
const { status } = await this.api.parseError(e);
|
||||
return status === 404 ? [] : null;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchCapabilities() {
|
||||
const { pathFor } = this.capabilities;
|
||||
const backend = this.secretMountPath.currentPath;
|
||||
// the issuers list endpoint is unauthenticated so we do not need to check capabilities for it
|
||||
const pathMap = {
|
||||
certificates: pathFor('pkiCertificates', { backend }),
|
||||
roles: pathFor('pkiRoles', { backend }),
|
||||
};
|
||||
const apiPaths = Object.values(pathMap);
|
||||
const perms = await this.capabilities.fetch(apiPaths, {
|
||||
routeForCache: 'vault.cluster.secrets.backend.pki.overview',
|
||||
});
|
||||
return {
|
||||
canListCertificates: perms[pathMap.certificates].canList,
|
||||
canListRoles: perms[pathMap.roles].canList,
|
||||
};
|
||||
}
|
||||
|
||||
async model() {
|
||||
const { canListCertificates, canListRoles } = await this.fetchCapabilities();
|
||||
return hash({
|
||||
hasConfig: this.pkiMountHasConfig,
|
||||
engine: this.modelFor('application'),
|
||||
roles: this.fetchAllRoles(),
|
||||
roles: canListRoles ? this.fetchAllRoles() : null,
|
||||
issuers: this.fetchAllIssuers(),
|
||||
certificates: this.fetchAllCertificates(),
|
||||
certificates: canListCertificates ? this.fetchAllCertificates() : null,
|
||||
canListCertificates,
|
||||
canListRoles,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
@backend={{this.model.parentModel.id}}
|
||||
@canImportKeys={{this.model.canImportKeys}}
|
||||
@canGenerateKeys={{this.model.canGenerateKeys}}
|
||||
@canRead={{this.model.canRead}}
|
||||
@canEdit={{this.model.canEdit}}
|
||||
@keyPermsById={{this.model.keyPermsById}}
|
||||
@hasConfig={{this.model.hasConfig}}
|
||||
/>
|
||||
|
|
@ -13,6 +13,8 @@
|
|||
@roles={{this.model.roles}}
|
||||
@certificates={{this.model.certificates}}
|
||||
@engine={{this.model.engine}}
|
||||
@canListCertificates={{this.model.canListCertificates}}
|
||||
@canListRoles={{this.model.canListRoles}}
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>
|
||||
|
|
|
|||
|
|
@ -80,10 +80,11 @@ module('Acceptance | pki overview', function (hooks) {
|
|||
assert.dom(`${overviewCard.container('Roles')} p`).hasText('1');
|
||||
});
|
||||
|
||||
test('hides roles card if user does not have permissions', async function (assert) {
|
||||
test('hides roles and certificates card if user does not have permissions', async function (assert) {
|
||||
await login(this.pkiIssuersList);
|
||||
await visit(`/vault/secrets-engines/${this.mountPath}/pki/overview`);
|
||||
assert.dom(overviewCard.title('Roles')).doesNotExist('Roles card does not exist');
|
||||
assert.dom(overviewCard.title('Certificates')).doesNotExist('Certificates card does not exist');
|
||||
assert.dom(overviewCard.title('Issuers')).hasText('Issuers');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,8 +34,26 @@ module('Integration | Component | pki key list page', function (hooks) {
|
|||
this.keys.meta = STANDARD_META;
|
||||
this.canImportKeys = true;
|
||||
this.canGenerateKeys = true;
|
||||
this.canRead = true;
|
||||
this.canEdit = true;
|
||||
this.keyPermsById = {
|
||||
'724862ff-6438-bad0-b598-77a6c7f4e934': {
|
||||
canCreate: true,
|
||||
canDelete: true,
|
||||
canList: true,
|
||||
canPatch: true,
|
||||
canRead: true,
|
||||
canSudo: true,
|
||||
canUpdate: true,
|
||||
},
|
||||
'9fdddf12-9ce3-0268-6b34-dc1553b00175': {
|
||||
canCreate: true,
|
||||
canDelete: true,
|
||||
canList: true,
|
||||
canPatch: true,
|
||||
canRead: true,
|
||||
canSudo: true,
|
||||
canUpdate: true,
|
||||
},
|
||||
};
|
||||
|
||||
this.renderComponent = () =>
|
||||
render(
|
||||
|
|
@ -45,8 +63,7 @@ module('Integration | Component | pki key list page', function (hooks) {
|
|||
@mountPoint="vault.cluster.secrets.backend.pki"
|
||||
@canImportKeys={{this.canImportKeys}}
|
||||
@canGenerateKeys={{this.canGenerateKeys}}
|
||||
@canRead={{this.canRead}}
|
||||
@canEdit={{this.canEdit}}
|
||||
@keyPermsById={{this.keyPermsById}}
|
||||
/>,
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
|
|
@ -92,9 +109,10 @@ module('Integration | Component | pki key list page', function (hooks) {
|
|||
|
||||
this.canImportKeys = false;
|
||||
this.canGenerateKeys = false;
|
||||
this.canRead = false;
|
||||
this.canEdit = false;
|
||||
|
||||
this.keyPermsById = {
|
||||
'724862ff-6438-bad0-b598-77a6c7f4e934': { canRead: false, canUpdate: false },
|
||||
'9fdddf12-9ce3-0268-6b34-dc1553b00175': { canRead: false, canUpdate: false },
|
||||
};
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(PKI_KEYS.importKey).doesNotExist('renders import action');
|
||||
|
|
|
|||
|
|
@ -24,10 +24,19 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
|
|||
this.roles = ['role-0', 'role-1', 'role-2'];
|
||||
this.certificates = ['22:2222:22222:2222', '33:3333:33333:3333'];
|
||||
this.engineId = 'pki';
|
||||
this.canListCertificates = true;
|
||||
this.canListRoles = true;
|
||||
|
||||
this.renderComponent = () =>
|
||||
render(
|
||||
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @cretificates={{this.certificates}} @engine={{this.engineId}} />`,
|
||||
hbs`<Page::PkiOverview
|
||||
@issuers={{this.issuers}}
|
||||
@roles={{this.roles}}
|
||||
@certificates={{this.certificates}}
|
||||
@engine={{this.engineId}}
|
||||
@canListCertificates={{this.canListCertificates}}
|
||||
@canListRoles={{this.canListRoles}}
|
||||
/>`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
});
|
||||
|
|
@ -39,6 +48,14 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
|
|||
.hasText(
|
||||
'Issuers View issuers The total number of issuers in this PKI mount. Includes both root and intermediate certificates. 2'
|
||||
);
|
||||
|
||||
this.issuers = [];
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom(overviewCard.container('Issuers'))
|
||||
.hasText(
|
||||
'Issuers View issuers The total number of issuers in this PKI mount. Includes both root and intermediate certificates. 0'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows the correct information on roles card', async function (assert) {
|
||||
|
|
@ -48,7 +65,7 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
|
|||
.hasText(
|
||||
'Roles View roles The total number of roles in this PKI mount that have been created to generate certificates. 3'
|
||||
);
|
||||
this.roles = 404;
|
||||
this.roles = [];
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom(overviewCard.container('Roles'))
|
||||
|
|
@ -57,17 +74,42 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
|
|||
);
|
||||
});
|
||||
|
||||
test('shows the input search fields for View Certificates card', async function (assert) {
|
||||
test('shows the search select dropdown for View Certificates card', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(overviewCard.title('View certificate')).hasText('View certificate');
|
||||
assert
|
||||
.dom(overviewCard.description('View certificate'))
|
||||
.hasText('Quickly view a certificate by looking up its serial number.');
|
||||
assert.dom(PKI_OVERVIEW.viewCertificateInput).exists();
|
||||
assert.dom(GENERAL.inputSearch('certificate')).doesNotExist('it does not render certificate input');
|
||||
assert.dom(PKI_OVERVIEW.viewCertificateButton).hasText('View');
|
||||
});
|
||||
|
||||
test('shows the search select dropdown for Issue Certificates card', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(overviewCard.title('Issue certificate')).hasText('Issue certificate');
|
||||
assert
|
||||
.dom(overviewCard.description('Issue certificate'))
|
||||
.hasText('Begin issuing a certificate by choosing a role.');
|
||||
assert.dom(PKI_OVERVIEW.issueCertificateInput).exists();
|
||||
assert.dom(GENERAL.inputSearch('role')).doesNotExist('it does not render role input');
|
||||
assert.dom(PKI_OVERVIEW.issueCertificateButton).hasText('Issue');
|
||||
});
|
||||
|
||||
test('shows the input search fields for Issue Certificates card', async function (assert) {
|
||||
test('it renders manual search inputs when no list permission', async function (assert) {
|
||||
this.canListCertificates = false;
|
||||
this.canListRoles = false;
|
||||
await this.renderComponent();
|
||||
assert.dom(overviewCard.title('View certificate')).hasText('View certificate');
|
||||
assert.dom(PKI_OVERVIEW.viewCertificateInput).exists();
|
||||
assert.dom(PKI_OVERVIEW.viewCertificateButton).hasText('View');
|
||||
assert.dom(overviewCard.container('Roles')).doesNotExist();
|
||||
assert
|
||||
.dom(overviewCard.description('View certificate'))
|
||||
.hasText('Quickly view a certificate by providing its serial number.');
|
||||
assert
|
||||
.dom(overviewCard.description('Issue certificate'))
|
||||
.hasText('Begin issuing a certificate by entering a role.');
|
||||
assert.dom(PKI_OVERVIEW.issueCertificateInput).doesNotExist('role search select does not render');
|
||||
assert.dom(GENERAL.inputSearch('role')).exists('it renders input instead of search select');
|
||||
assert.dom(PKI_OVERVIEW.viewCertificateInput).doesNotExist('certificate search select does not render');
|
||||
assert.dom(GENERAL.inputSearch('certificate')).exists('it renders input instead of search selects');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue