mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 09:42:25 +00:00
One WIF configuration component (#29367)
* make one component and make one test file for that component. remove the two components and associated files the new component replaces * make access type subtext dynamic based on model type * clean up * clean up * remove model attr for display purposes * split out lease to another second config model type and make is-wif-engine helper * welp missed the old controller * small removal of overkill comment * pr feedback * save lease config if only thing changed * error handling in acceptance test * test fix * replace notOk with throw * move back error message * clean up focused largely on wif component test * replace ok with true
This commit is contained in:
@@ -1,136 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<form {{on "submit" (perform this.submitForm)}} aria-label="save aws creds" data-test-root-form>
|
||||
<div class="box is-fullwidth is-shadowless is-marginless">
|
||||
<NamespaceReminder @mode="save" @noun="configuration" />
|
||||
<MessageError @errorMessage={{this.errorMessageRoot}} />
|
||||
<p class="has-text-grey-dark">
|
||||
Note: the client uses the official AWS SDK and will use the specified credentials, environment credentials, shared file
|
||||
credentials, or IAM role/ECS task credentials in that order.
|
||||
</p>
|
||||
</div>
|
||||
{{! Root configuration details }}
|
||||
<h2 class="title is-5 has-bottom-padding-s has-top-margin-l" data-test-access-title>
|
||||
Access to AWS
|
||||
</h2>
|
||||
<div class="box is-fullwidth is-sideless">
|
||||
{{! WIF is an enterprise only feature. We default to IAM access type for community users and display only those related form fields. }}
|
||||
{{#if this.version.isEnterprise}}
|
||||
<fieldset class="field form-fieldset" id="protection" data-test-access-type-section>
|
||||
<legend class="is-label">Access Type</legend>
|
||||
<p class="sub-text" data-test-access-type-subtext>
|
||||
{{#if this.disableAccessType}}
|
||||
You cannot edit Access Type if you have already saved access credentials.
|
||||
{{else}}
|
||||
Choose the way to configure access to AWS. Access can be configured either with IAM access keys, or using Plugin
|
||||
Workload Identity Federation (WIF).{{/if}}</p>
|
||||
<div>
|
||||
<RadioButton
|
||||
id="access-type-iam"
|
||||
name="iam"
|
||||
class="radio"
|
||||
data-test-access-type="iam"
|
||||
@value="iam"
|
||||
@groupValue={{this.accessType}}
|
||||
@onChange={{fn this.onChangeAccessType "iam"}}
|
||||
@disabled={{this.disableAccessType}}
|
||||
/>
|
||||
<label for="access-type-iam">IAM Credentials</label>
|
||||
|
||||
<RadioButton
|
||||
id="access-type-wif"
|
||||
name="wif"
|
||||
class="radio has-left-margin-m"
|
||||
data-test-access-type="wif"
|
||||
@value="wif"
|
||||
@groupValue={{this.accessType}}
|
||||
@onChange={{fn this.onChangeAccessType "wif"}}
|
||||
@disabled={{this.disableAccessType}}
|
||||
/>
|
||||
<label for="access-type-wif">Workload Identity Federation</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
{{/if}}
|
||||
{{#if (eq this.accessType "wif")}}
|
||||
{{! WIF Fields }}
|
||||
{{#each @issuerConfig.displayAttrs as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@issuerConfig}} />
|
||||
{{/each}}
|
||||
<FormFieldGroups
|
||||
@model={{@rootConfig}}
|
||||
@mode={{if @rootConfig.isNew "create" "edit"}}
|
||||
@modelValidations={{this.modelValidationsRoot}}
|
||||
@groupName="fieldGroupsWif"
|
||||
/>
|
||||
{{else}}
|
||||
{{! IAM Fields }}
|
||||
<FormFieldGroups
|
||||
@model={{@rootConfig}}
|
||||
@mode={{if @rootConfig.isNew "create" "edit"}}
|
||||
@modelValidations={{this.modelValidationsRoot}}
|
||||
@useEnableInput={{true}}
|
||||
@groupName="fieldGroupsIam"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{! Lease configuration details }}
|
||||
<h2 class="title is-5 has-bottom-padding-s has-top-margin-l" data-test-lease-title>
|
||||
Leases
|
||||
</h2>
|
||||
<div class="box is-fullwidth is-sideless is-bottomless">
|
||||
{{#each @leaseConfig.displayAttrs as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@leaseConfig}} @modelValidations={{this.modelValidationsLease}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<Hds::Button
|
||||
@text="Save"
|
||||
@icon={{if this.save.isRunning "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-save
|
||||
/>
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
class="has-left-margin-s"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.onCancel}}
|
||||
data-test-cancel
|
||||
/>
|
||||
</div>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<AlertInline
|
||||
data-test-invalid-form-alert
|
||||
class="has-top-padding-s"
|
||||
@type="danger"
|
||||
@message={{this.invalidFormAlert}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{#if this.saveIssuerWarning}}
|
||||
<Hds::Modal @color="warning" @onClose={{action (mut this.saveIssuerWarning) ""}} data-test-issuer-warning as |M|>
|
||||
<M.Header @icon="alert-circle">
|
||||
Are you sure?
|
||||
</M.Header>
|
||||
<M.Body>
|
||||
<p class="has-bottom-margin-s" data-test-issuer-warning-message>
|
||||
{{this.saveIssuerWarning}}
|
||||
</p>
|
||||
</M.Body>
|
||||
<M.Footer as |F|>
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button @text="Continue" {{on "click" this.continueSubmitForm}} data-test-issuer-save />
|
||||
<Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} data-test-issuer-cancel />
|
||||
</Hds::ButtonSet>
|
||||
</M.Footer>
|
||||
</Hds::Modal>
|
||||
{{/if}}
|
||||
@@ -1,232 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { ValidationMap } from 'vault/vault/app-types';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
import type LeaseConfigModel from 'vault/models/aws/lease-config';
|
||||
import type RootConfigModel from 'vault/models/aws/root-config';
|
||||
import type IdentityOidcConfigModel from 'vault/models/identity/oidc/config';
|
||||
import type Router from '@ember/routing/router';
|
||||
import type StoreService from 'vault/services/store';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
|
||||
/**
|
||||
* @module ConfigureAwsComponent is used to configure the AWS secret engine
|
||||
* A user can configure the endpoint root/config and/or lease/config.
|
||||
* For enterprise users, they will see an additional option to config WIF attributes in place of IAM attributes.
|
||||
* The fields for these endpoints are on one form.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <SecretEngine::ConfigureAws
|
||||
@rootConfig={{this.model.aws-root-config}}
|
||||
@leaseConfig={{this.model.aws-lease-config}}
|
||||
@backendPath={{this.model.id}}
|
||||
/>
|
||||
* ```
|
||||
*
|
||||
* @param {object} rootConfig - AWS config/root model
|
||||
* @param {object} leaseConfig - AWS config/lease model
|
||||
* @param {string} backendPath - name of the AWS secret engine, ex: 'aws-123'
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
leaseConfig: LeaseConfigModel;
|
||||
rootConfig: RootConfigModel;
|
||||
issuerConfig: IdentityOidcConfigModel;
|
||||
backendPath: string;
|
||||
issuer?: string;
|
||||
}
|
||||
|
||||
export default class ConfigureAwsComponent extends Component<Args> {
|
||||
@service declare readonly router: Router;
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly version: VersionService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked errorMessageRoot: string | null = null;
|
||||
@tracked errorMessageLease: string | null = null;
|
||||
@tracked invalidFormAlert: string | null = null;
|
||||
@tracked modelValidationsLease: ValidationMap | null = null;
|
||||
@tracked accessType = 'iam';
|
||||
@tracked saveIssuerWarning = '';
|
||||
|
||||
disableAccessType = false;
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
// the following checks are only relevant to enterprise users and those editing an existing root configuration.
|
||||
if (this.version.isCommunity || this.args.rootConfig.isNew) return;
|
||||
const { roleArn, identityTokenAudience, identityTokenTtl, accessKey } = this.args.rootConfig;
|
||||
// do not include issuer in this check. Issuer is a global endpoint and can be set even if we're not editing wif attributes
|
||||
const wifAttributesSet = !!roleArn || !!identityTokenAudience || !!identityTokenTtl;
|
||||
const iamAttributesSet = !!accessKey;
|
||||
// If any WIF attributes have been set in the rootConfig model, set accessType to 'wif'.
|
||||
this.accessType = wifAttributesSet ? 'wif' : 'iam';
|
||||
// If there are either WIF or IAM attributes set then disable user's ability to change accessType.
|
||||
this.disableAccessType = wifAttributesSet || iamAttributesSet;
|
||||
}
|
||||
|
||||
@action continueSubmitForm() {
|
||||
// called when the user confirms they are okay with the issuer change
|
||||
this.saveIssuerWarning = '';
|
||||
this.save.perform();
|
||||
}
|
||||
|
||||
// on form submit - validate inputs and check for issuer changes
|
||||
submitForm = task(
|
||||
waitFor(async (event: Event) => {
|
||||
event?.preventDefault();
|
||||
this.resetErrors();
|
||||
const { leaseConfig, issuerConfig } = this.args;
|
||||
// Note: only aws/lease-config model has validations
|
||||
const isValid = this.validate(leaseConfig);
|
||||
if (!isValid) return;
|
||||
if (issuerConfig?.hasDirtyAttributes) {
|
||||
// if the issuer has changed show modal with warning that the config will change
|
||||
// if the modal is shown, the user has to click confirm to continue save
|
||||
this.saveIssuerWarning = `You are updating the global issuer config. This will overwrite Vault's current issuer ${
|
||||
issuerConfig.queryIssuerError ? 'if it exists ' : ''
|
||||
}and may affect other configurations using this value. Continue?`;
|
||||
// exit task until user confirms
|
||||
return;
|
||||
}
|
||||
await this.save.perform();
|
||||
})
|
||||
);
|
||||
|
||||
save = task(
|
||||
waitFor(async () => {
|
||||
// when we get here, the models have already been validated so just continue with save
|
||||
const { leaseConfig, rootConfig, issuerConfig } = this.args;
|
||||
// Check if any of the models' attributes have changed.
|
||||
// If no changes to either model, transition and notify user.
|
||||
// If changes to either model, save the model(s) that changed and notify user.
|
||||
// Note: "backend" dirties model state so explicity ignore it here.
|
||||
const leaseAttrChanged = Object.keys(leaseConfig?.changedAttributes()).some(
|
||||
(item) => item !== 'backend'
|
||||
);
|
||||
const rootAttrChanged = Object.keys(rootConfig?.changedAttributes()).some((item) => item !== 'backend');
|
||||
const issuerAttrChanged = issuerConfig?.hasDirtyAttributes;
|
||||
if (!leaseAttrChanged && !rootAttrChanged && !issuerAttrChanged) {
|
||||
this.flashMessages.info('No changes detected.');
|
||||
this.transition();
|
||||
return;
|
||||
}
|
||||
// Attempt saves of changed models. If at least one of them succeed, transition
|
||||
const rootSaved = rootAttrChanged ? await this.saveRoot() : false;
|
||||
const leaseSaved = leaseAttrChanged ? await this.saveLease() : false;
|
||||
const issuerSaved = issuerAttrChanged ? await this.updateIssuer() : false;
|
||||
|
||||
if (rootSaved || leaseSaved || issuerSaved) {
|
||||
this.transition();
|
||||
} else {
|
||||
// otherwise there was a failure and we should not transition and exit the function.
|
||||
return;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
async updateIssuer(): Promise<boolean> {
|
||||
try {
|
||||
await this.args.issuerConfig.save();
|
||||
this.flashMessages.success('Issuer saved successfully');
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.flashMessages.danger(`Issuer was not saved: ${errorMessage(e, 'Check Vault logs for details.')}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveRoot(): Promise<boolean> {
|
||||
const { backendPath, rootConfig } = this.args;
|
||||
try {
|
||||
await rootConfig.save();
|
||||
this.flashMessages.success(`Successfully saved ${backendPath}'s root configuration.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.errorMessageRoot = errorMessage(error);
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveLease(): Promise<boolean> {
|
||||
const { backendPath, leaseConfig } = this.args;
|
||||
try {
|
||||
await leaseConfig.save();
|
||||
this.flashMessages.success(`Successfully saved ${backendPath}'s lease configuration.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// if lease config fails, but there was no error saving rootConfig: notify user of the lease failure with a flash message, save the root config, and transition.
|
||||
if (!this.errorMessageRoot) {
|
||||
this.flashMessages.danger(`Lease configuration was not saved: ${errorMessage(error)}`, {
|
||||
sticky: true,
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
this.errorMessageLease = errorMessage(error);
|
||||
this.flashMessages.danger(
|
||||
`Configuration not saved: ${errorMessage(error)}. ${this.errorMessageRoot}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetErrors() {
|
||||
this.flashMessages.clearMessages();
|
||||
this.errorMessageRoot = null;
|
||||
this.invalidFormAlert = null;
|
||||
}
|
||||
|
||||
transition() {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath);
|
||||
}
|
||||
|
||||
validate(model: LeaseConfigModel) {
|
||||
const { isValid, state, invalidFormMessage } = model.validate();
|
||||
this.modelValidationsLease = isValid ? null : state;
|
||||
this.invalidFormAlert = isValid ? '' : invalidFormMessage;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
unloadModels() {
|
||||
this.args.rootConfig.unloadRecord();
|
||||
this.args.leaseConfig.unloadRecord();
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeAccessType(accessType: string) {
|
||||
this.accessType = accessType;
|
||||
const { rootConfig } = this.args;
|
||||
if (accessType === 'iam') {
|
||||
// reset all WIF attributes
|
||||
rootConfig.roleArn = rootConfig.identityTokenAudience = rootConfig.identityTokenTtl = undefined;
|
||||
// for the issuer return to the globally set value (if there is one) on toggle
|
||||
this.args.issuerConfig.rollbackAttributes();
|
||||
}
|
||||
if (accessType === 'wif') {
|
||||
// reset all IAM attributes
|
||||
rootConfig.accessKey = rootConfig.secretKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onCancel() {
|
||||
// clear errors because they're canceling out of the workflow.
|
||||
this.resetErrors();
|
||||
this.unloadModels();
|
||||
this.transition();
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
import type ConfigModel from 'vault/models/azure/config';
|
||||
import type IdentityOidcConfigModel from 'vault/models/identity/oidc/config';
|
||||
import type Router from '@ember/routing/router';
|
||||
import type StoreService from 'vault/services/store';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
|
||||
/**
|
||||
* @module SecretEngineConfigureAzure component is used to configure the Azure secret engine
|
||||
* For enterprise users, they will see an additional option to config WIF attributes in place of Azure account attributes.
|
||||
* If the user is configuring WIF attributes they will also have the option to update the global issuer config, which is a separate endpoint named identity/oidc/config.
|
||||
* @example
|
||||
* <SecretEngine::ConfigureAzure
|
||||
@model={{this.model.azure-config}}
|
||||
@backendPath={{this.model.id}}
|
||||
@issuerConfig={{this.model.identity-oidc-config}}
|
||||
/>
|
||||
*
|
||||
* @param {object} model - Azure config model
|
||||
* @param {string} backendPath - name of the Azure secret engine, ex: 'azure-123'
|
||||
* @param {object} issuerConfigModel - the identity/oidc/config model
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
model: ConfigModel;
|
||||
issuerConfig: IdentityOidcConfigModel;
|
||||
backendPath: string;
|
||||
}
|
||||
|
||||
export default class ConfigureAzureComponent extends Component<Args> {
|
||||
@service declare readonly router: Router;
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly version: VersionService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked accessType = 'azure';
|
||||
@tracked errorMessage = '';
|
||||
@tracked invalidFormAlert = '';
|
||||
@tracked saveIssuerWarning = '';
|
||||
|
||||
disableAccessType = false;
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
// the following checks are only relevant to existing enterprise configurations
|
||||
if (this.version.isCommunity && this.args.model.isNew) return;
|
||||
const { isWifPluginConfigured, isAzureAccountConfigured } = this.args.model;
|
||||
this.accessType = isWifPluginConfigured ? 'wif' : 'azure';
|
||||
// if there are either WIF or azure attributes, disable user's ability to change accessType
|
||||
this.disableAccessType = isWifPluginConfigured || isAzureAccountConfigured;
|
||||
}
|
||||
|
||||
get modelAttrChanged() {
|
||||
// "backend" dirties model state so explicity ignore it here
|
||||
return Object.keys(this.args.model?.changedAttributes()).some((item) => item !== 'backend');
|
||||
}
|
||||
|
||||
get issuerAttrChanged() {
|
||||
return this.args.issuerConfig?.hasDirtyAttributes;
|
||||
}
|
||||
|
||||
@action continueSubmitForm() {
|
||||
this.saveIssuerWarning = '';
|
||||
this.save.perform();
|
||||
}
|
||||
|
||||
// check if the issuer has been changed to show issuer modal
|
||||
// continue saving the configuration
|
||||
submitForm = task(
|
||||
waitFor(async (event: Event) => {
|
||||
event?.preventDefault();
|
||||
this.resetErrors();
|
||||
|
||||
if (this.issuerAttrChanged) {
|
||||
// if the issuer has changed show modal with warning that the config will change
|
||||
// if the modal is shown, the user has to click confirm to continue saving
|
||||
this.saveIssuerWarning = `You are updating the global issuer config. This will overwrite Vault's current issuer ${
|
||||
this.args.issuerConfig.queryIssuerError ? 'if it exists ' : ''
|
||||
}and may affect other configurations using this value. Continue?`;
|
||||
// exit task until user confirms
|
||||
return;
|
||||
}
|
||||
await this.save.perform();
|
||||
})
|
||||
);
|
||||
|
||||
save = task(
|
||||
waitFor(async () => {
|
||||
const modelAttrChanged = this.modelAttrChanged;
|
||||
const issuerAttrChanged = this.issuerAttrChanged;
|
||||
// check if any of the model or issue attributes have changed
|
||||
// if no changes, transition and notify user
|
||||
if (!modelAttrChanged && !issuerAttrChanged) {
|
||||
this.flashMessages.info('No changes detected.');
|
||||
this.transition();
|
||||
return;
|
||||
}
|
||||
|
||||
const modelSaved = modelAttrChanged ? await this.saveModel() : false;
|
||||
const issuerSaved = issuerAttrChanged ? await this.updateIssuer() : false;
|
||||
|
||||
if (modelSaved || (!modelAttrChanged && issuerSaved)) {
|
||||
// transition if the model was saved successfully
|
||||
// we only prevent a transition if the model is edited and fails saving
|
||||
this.transition();
|
||||
} else {
|
||||
// otherwise there was a failure and we should not transition and exit the function
|
||||
return;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
async updateIssuer(): Promise<boolean> {
|
||||
try {
|
||||
await this.args.issuerConfig.save();
|
||||
this.flashMessages.success('Issuer saved successfully');
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.flashMessages.danger(`Issuer was not saved: ${errorMessage(e, 'Check Vault logs for details.')}`);
|
||||
// remove issuer from the config model if it was not saved
|
||||
this.args.issuerConfig.rollbackAttributes();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveModel(): Promise<boolean> {
|
||||
const { backendPath, model } = this.args;
|
||||
try {
|
||||
await model.save();
|
||||
this.flashMessages.success(`Successfully saved ${backendPath}'s configuration.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.errorMessage = errorMessage(error);
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
resetErrors() {
|
||||
this.flashMessages.clearMessages();
|
||||
this.errorMessage = '';
|
||||
this.invalidFormAlert = '';
|
||||
}
|
||||
|
||||
transition() {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath);
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeAccessType(accessType: string) {
|
||||
this.accessType = accessType;
|
||||
const { model } = this.args;
|
||||
if (accessType === 'azure') {
|
||||
// reset all WIF attributes
|
||||
model.identityTokenAudience = model.identityTokenTtl = undefined;
|
||||
// return the issuer to the globally set value (if there is one) on toggle
|
||||
this.args.issuerConfig.rollbackAttributes();
|
||||
}
|
||||
if (accessType === 'wif') {
|
||||
// reset all Azure attributes
|
||||
model.clientSecret = model.rootPasswordTtl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onCancel() {
|
||||
this.resetErrors();
|
||||
this.args.model.unloadRecord();
|
||||
this.transition();
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,21 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<form {{on "submit" (perform this.submitForm)}} aria-label="configure azure credentials" data-test-configure-form>
|
||||
<form {{on "submit" (perform this.submitForm)}} aria-label="configure {{@type}} credentials" data-test-configure-form>
|
||||
<NamespaceReminder @mode="save" @noun="configuration" />
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
{{! AWS specific note and section header }}
|
||||
{{#if (eq @type "aws")}}
|
||||
<p class="has-text-grey-dark has-top-bottom-margin" data-test-configure-note={{@type}}>
|
||||
Note: the client uses the official AWS SDK and will use the specified credentials, environment credentials, shared file
|
||||
credentials, or IAM role/ECS task credentials in that order.
|
||||
</p>
|
||||
<h2 class="title is-5 has-bottom-padding-s has-top-margin-l" data-test-access-title>
|
||||
Access to AWS
|
||||
</h2>
|
||||
{{/if}}
|
||||
<div class="box is-fullwidth is-sideless">
|
||||
{{! accessType can be "azure" or "wif" - since WIF is an enterprise only feature we default to "azure" for community users and only display those related form fields. }}
|
||||
{{! Only enterprise users can change access type from "account" to "wif" }}
|
||||
{{#if this.version.isEnterprise}}
|
||||
<fieldset class="field form-fieldset" id="protection" data-test-access-type-section>
|
||||
<legend class="is-label">Access Type</legend>
|
||||
@@ -15,22 +25,24 @@
|
||||
{{#if this.disableAccessType}}
|
||||
You cannot edit Access Type if you have already saved access credentials.
|
||||
{{else}}
|
||||
Choose the way to configure access to Azure. Access can be configured either using an Azure account or with the
|
||||
Plugin Workload Identity Federation (WIF).
|
||||
Choose the way to configure access to
|
||||
{{@displayName}}. Access can be configured either using
|
||||
{{if (eq @type "aws") "IAM access keys" (concat @displayName " account credentials")}}
|
||||
or with the Plugin Workload Identity Federation (WIF).
|
||||
{{/if}}
|
||||
</p>
|
||||
<div>
|
||||
<RadioButton
|
||||
id="access-type-azure"
|
||||
name="azure"
|
||||
id="access-type-{{@type}}"
|
||||
name="account"
|
||||
class="radio"
|
||||
data-test-access-type="azure"
|
||||
@value="azure"
|
||||
data-test-access-type={{@type}}
|
||||
@value="account"
|
||||
@groupValue={{this.accessType}}
|
||||
@onChange={{fn this.onChangeAccessType "azure"}}
|
||||
@onChange={{this.onChangeAccessType}}
|
||||
@disabled={{this.disableAccessType}}
|
||||
/>
|
||||
<label for="access-type-azure">Azure account credentials</label>
|
||||
<label for="access-type-{{@type}}">{{@displayName}} account credentials</label>
|
||||
<RadioButton
|
||||
id="access-type-wif"
|
||||
name="wif"
|
||||
@@ -38,7 +50,7 @@
|
||||
data-test-access-type="wif"
|
||||
@value="wif"
|
||||
@groupValue={{this.accessType}}
|
||||
@onChange={{fn this.onChangeAccessType "wif"}}
|
||||
@onChange={{this.onChangeAccessType}}
|
||||
@disabled={{this.disableAccessType}}
|
||||
/>
|
||||
<label for="access-type-wif">Workload Identity Federation</label>
|
||||
@@ -46,21 +58,38 @@
|
||||
</fieldset>
|
||||
{{/if}}
|
||||
{{#if (eq this.accessType "wif")}}
|
||||
{{! WIF Fields }}
|
||||
{{! if access type is "wif" display Issuer and WIF fields }}
|
||||
{{#each @issuerConfig.displayAttrs as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@issuerConfig}} />
|
||||
{{/each}}
|
||||
<FormFieldGroups @model={{@model}} @mode={{if @model.isNew "create" "edit"}} @groupName="fieldGroupsWif" />
|
||||
{{else}}
|
||||
{{! Azure Account Fields }}
|
||||
<FormFieldGroups
|
||||
@model={{@model}}
|
||||
@mode={{if @model.isNew "create" "edit"}}
|
||||
@model={{@mountConfigModel}}
|
||||
@mode={{if @mountConfigModel.isNew "create" "edit"}}
|
||||
@groupName="fieldGroupsWif"
|
||||
/>
|
||||
{{else}}
|
||||
{{! otherwise display account credential fields }}
|
||||
<FormFieldGroups
|
||||
@model={{@mountConfigModel}}
|
||||
@mode={{if @mountConfigModel.isNew "create" "edit"}}
|
||||
@useEnableInput={{true}}
|
||||
@groupName="fieldGroupsAzure"
|
||||
@groupName="fieldGroupsAccount"
|
||||
/>
|
||||
{{/if}}
|
||||
{{! additionalConfigModel fields show regardless of the vault version or what access type is selected }}
|
||||
{{#if @additionalConfigModel}}
|
||||
<h2 class="title is-5 has-bottom-padding-s has-top-margin-l" data-test-additional-config-model-title>
|
||||
{{if (eq @type "aws") "Lease" "Additional"}}
|
||||
Configuration
|
||||
</h2>
|
||||
<div class="box is-fullwidth is-sideless is-bottomless">
|
||||
{{#each @additionalConfigModel.formFields as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@additionalConfigModel}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@text="Save"
|
||||
252
ui/app/components/secret-engine/configure-wif.ts
Normal file
252
ui/app/components/secret-engine/configure-wif.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { ValidationMap } from 'vault/vault/app-types';
|
||||
import { assert } from '@ember/debug';
|
||||
import { capitalize } from '@ember/string';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
import type MountConfigModel from 'vault/vault/models/secret-engine/mount-config';
|
||||
import type AdditionalConfigModel from 'vault/vault/models/secret-engine/additional-config';
|
||||
import type IdentityOidcConfigModel from 'vault/models/identity/oidc/config';
|
||||
import type Router from '@ember/routing/router';
|
||||
import type StoreService from 'vault/services/store';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
|
||||
/**
|
||||
* @module SecretEngineConfigureWif component is used to configure secret engines that allow the WIF (Workload Identity Federation) configuration.
|
||||
* The ability to configure WIF fields is an enterprise only feature.
|
||||
* If the user is configuring WIF attributes they will also have the option to update the global issuer config, which is a separate endpoint named identity/oidc/config.
|
||||
* If a user is on CE, the account configuration fields will display with no ability to select or see wif fields.
|
||||
*
|
||||
* @example
|
||||
* <SecretEngine::ConfigureWif
|
||||
@backendPath={{this.model.id}}
|
||||
@displayName="AWS"
|
||||
@type="aws"
|
||||
@mountConfigModel={{this.model.mount-config-model}}
|
||||
@additionalConfigModel={{this.model.additional-config-model}}
|
||||
@issuerConfig={{this.model.identity-oidc-config}}
|
||||
/>
|
||||
*
|
||||
* @param {string} backendPath - name of the secret engine, ex: 'azure-123'.
|
||||
* @param {string} displayName - used for flash messages, subText and labels. ex: 'Azure'.
|
||||
* @param {string} type - the type of the engine, ex: 'azure'.
|
||||
* @param {object} mountConfigModel - the config model for the engine. The attr `isWifPluginConfigured` must be added to this config model otherwise this component will assert an error. `isWifPluginConfigured` should return true if any required wif fields have been set.
|
||||
* @param {object} [additionalConfigModel] - for engines with two config models. Currently, only used by aws
|
||||
* @param {object} [issuerConfig] - the identity/oidc/config model. Will be passed in if user has an enterprise license.
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
backendPath: string;
|
||||
displayName: string;
|
||||
type: string;
|
||||
mountConfigModel: MountConfigModel;
|
||||
additionalConfigModel: AdditionalConfigModel;
|
||||
issuerConfig: IdentityOidcConfigModel;
|
||||
}
|
||||
|
||||
export default class ConfigureWif extends Component<Args> {
|
||||
@service declare readonly router: Router;
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly version: VersionService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked accessType = 'account'; // for community users they will not be able to change this. for enterprise users, they will have the option to select "wif".
|
||||
@tracked errorMessage = '';
|
||||
@tracked invalidFormAlert = '';
|
||||
@tracked saveIssuerWarning = '';
|
||||
@tracked modelValidations: ValidationMap | null = null;
|
||||
|
||||
disableAccessType = false;
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
// the following checks are only relevant to existing enterprise configurations
|
||||
if (this.version.isCommunity && this.args.mountConfigModel.isNew) return;
|
||||
const { isWifPluginConfigured, isAccountPluginConfigured } = this.args.mountConfigModel;
|
||||
assert(
|
||||
`'isWifPluginConfigured' is required to be defined on the config model. Must return a boolean.`,
|
||||
isWifPluginConfigured !== undefined
|
||||
);
|
||||
this.accessType = isWifPluginConfigured ? 'wif' : 'account';
|
||||
// if wif or account only attributes are defined, disable the user's ability to change the access type
|
||||
this.disableAccessType = isWifPluginConfigured || isAccountPluginConfigured;
|
||||
}
|
||||
|
||||
get mountConfigModelAttrChanged() {
|
||||
// "backend" dirties model state so explicity ignore it here
|
||||
return Object.keys(this.args.mountConfigModel?.changedAttributes()).some((item) => item !== 'backend');
|
||||
}
|
||||
|
||||
get issuerAttrChanged() {
|
||||
return this.args.issuerConfig?.hasDirtyAttributes;
|
||||
}
|
||||
|
||||
get additionalConfigModelAttrChanged() {
|
||||
const { additionalConfigModel } = this.args;
|
||||
// required to check for additional model otherwise Object.keys will have nothing to iterate over and fails
|
||||
return additionalConfigModel
|
||||
? Object.keys(additionalConfigModel.changedAttributes()).some((item) => item !== 'backend')
|
||||
: false;
|
||||
}
|
||||
|
||||
@action continueSubmitForm() {
|
||||
this.saveIssuerWarning = '';
|
||||
this.save.perform();
|
||||
}
|
||||
|
||||
// check if the issuer has been changed to show issuer modal
|
||||
// continue saving the configuration
|
||||
submitForm = task(
|
||||
waitFor(async (event: Event) => {
|
||||
event?.preventDefault();
|
||||
this.resetErrors();
|
||||
// currently we only check validations on the additional model
|
||||
if (this.args.additionalConfigModel && !this.isValid(this.args.additionalConfigModel)) {
|
||||
return;
|
||||
}
|
||||
if (this.issuerAttrChanged) {
|
||||
// if the issuer has changed show modal with warning that the config will change
|
||||
// if the modal is shown, the user has to click confirm to continue saving
|
||||
this.saveIssuerWarning = `You are updating the global issuer config. This will overwrite Vault's current issuer ${
|
||||
this.args.issuerConfig.queryIssuerError ? 'if it exists ' : ''
|
||||
}and may affect other configurations using this value. Continue?`;
|
||||
// exit task until user confirms
|
||||
return;
|
||||
}
|
||||
await this.save.perform();
|
||||
})
|
||||
);
|
||||
|
||||
save = task(
|
||||
waitFor(async () => {
|
||||
const mountConfigModelChanged = this.mountConfigModelAttrChanged;
|
||||
const additionalModelAttrChanged = this.additionalConfigModelAttrChanged;
|
||||
const issuerAttrChanged = this.issuerAttrChanged;
|
||||
// check if any of the model(s) or issuer attributes have changed
|
||||
// if no changes, transition and notify user
|
||||
if (!mountConfigModelChanged && !additionalModelAttrChanged && !issuerAttrChanged) {
|
||||
this.flashMessages.info('No changes detected.');
|
||||
this.transition();
|
||||
return;
|
||||
}
|
||||
|
||||
const mountConfigModelSaved = mountConfigModelChanged ? await this.saveMountConfigModel() : false;
|
||||
const issuerSaved = issuerAttrChanged ? await this.updateIssuer() : false;
|
||||
|
||||
if (
|
||||
mountConfigModelSaved ||
|
||||
(!mountConfigModelChanged && issuerSaved) ||
|
||||
(!mountConfigModelChanged && additionalModelAttrChanged)
|
||||
) {
|
||||
// if there are changes made to the an additional model, attempt to save it. if saving fails, we transition and the failure will surface as a sticky flash message on the configuration details page.
|
||||
if (additionalModelAttrChanged) {
|
||||
await this.saveAdditionalConfigModel();
|
||||
}
|
||||
// we only prevent a transition if the mount config model or issuer fail when saving
|
||||
this.transition();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
async updateIssuer(): Promise<boolean> {
|
||||
try {
|
||||
await this.args.issuerConfig.save();
|
||||
this.flashMessages.success('Issuer saved successfully');
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.flashMessages.danger(`Issuer was not saved: ${errorMessage(e, 'Check Vault logs for details.')}`);
|
||||
// remove issuer from the config model if it was not saved
|
||||
this.args.issuerConfig.rollbackAttributes();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveMountConfigModel(): Promise<boolean> {
|
||||
const { backendPath, mountConfigModel } = this.args;
|
||||
try {
|
||||
await mountConfigModel.save();
|
||||
this.flashMessages.success(`Successfully saved ${backendPath}'s configuration.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.errorMessage = errorMessage(error);
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveAdditionalConfigModel() {
|
||||
const { backendPath, additionalConfigModel, type } = this.args;
|
||||
const additionalConfigModelName = type === 'aws' ? 'lease configuration' : 'additional configuration';
|
||||
try {
|
||||
await additionalConfigModel.save();
|
||||
this.flashMessages.success(`Successfully saved ${backendPath}'s ${additionalConfigModelName}.`);
|
||||
} catch (error) {
|
||||
// the only error the user sees is a sticky flash message on the next view.
|
||||
this.flashMessages.danger(
|
||||
`${capitalize(additionalConfigModelName)} was not saved: ${errorMessage(error)}`,
|
||||
{
|
||||
sticky: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
resetErrors() {
|
||||
this.flashMessages.clearMessages();
|
||||
this.errorMessage = this.invalidFormAlert = '';
|
||||
this.modelValidations = null;
|
||||
}
|
||||
|
||||
transition() {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath);
|
||||
}
|
||||
|
||||
isValid(model: AdditionalConfigModel) {
|
||||
const { isValid, state, invalidFormMessage } = model.validate();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = isValid ? '' : invalidFormMessage;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeAccessType(accessType: string) {
|
||||
this.accessType = accessType;
|
||||
const { mountConfigModel, type } = this.args;
|
||||
if (accessType === 'account') {
|
||||
// reset all "wif" attributes that are mutually exclusive with "account" attributes
|
||||
// these attributes are the same for each engine
|
||||
mountConfigModel.identityTokenAudience = mountConfigModel.identityTokenTtl = undefined;
|
||||
// return the issuer to the globally set value (if there is one) on toggle
|
||||
this.args.issuerConfig.rollbackAttributes();
|
||||
}
|
||||
if (accessType === 'wif') {
|
||||
// reset all "account" attributes that are mutually exclusive with "wif" attributes
|
||||
// these attributes are different for each engine
|
||||
type === 'azure'
|
||||
? (mountConfigModel.clientSecret = mountConfigModel.rootPasswordTtl = undefined)
|
||||
: type === 'aws'
|
||||
? (mountConfigModel.accessKey = undefined)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onCancel() {
|
||||
this.resetErrors();
|
||||
this.args.mountConfigModel.unloadRecord();
|
||||
this.transition();
|
||||
}
|
||||
}
|
||||
@@ -3,54 +3,14 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { isPresent } from '@ember/utils';
|
||||
import { service } from '@ember/service';
|
||||
import Controller from '@ember/controller';
|
||||
import { WIF_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines';
|
||||
|
||||
const CONFIG_ATTRS = {
|
||||
// ssh
|
||||
configured: false,
|
||||
|
||||
// aws root config
|
||||
iamEndpoint: null,
|
||||
stsEndpoint: null,
|
||||
accessKey: null,
|
||||
secretKey: null,
|
||||
region: '',
|
||||
};
|
||||
|
||||
export default Controller.extend(CONFIG_ATTRS, {
|
||||
queryParams: ['tab'],
|
||||
tab: '',
|
||||
flashMessages: service(),
|
||||
loading: false,
|
||||
reset() {
|
||||
this.model.rollbackAttributes();
|
||||
this.setProperties(CONFIG_ATTRS);
|
||||
},
|
||||
actions: {
|
||||
save(method, data) {
|
||||
this.set('loading', true);
|
||||
const hasData = Object.keys(data).some((key) => {
|
||||
return isPresent(data[key]);
|
||||
});
|
||||
if (!hasData) {
|
||||
return;
|
||||
}
|
||||
this.model
|
||||
.save({
|
||||
adapterOptions: {
|
||||
adapterMethod: method,
|
||||
data,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.reset();
|
||||
this.flashMessages.success('The backend configuration saved successfully!');
|
||||
})
|
||||
.finally(() => {
|
||||
this.set('loading', false);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
export default class SecretsBackendConfigurationEditController extends Controller {
|
||||
get isWifEngine() {
|
||||
return WIF_ENGINES.includes(this.model.type);
|
||||
}
|
||||
get displayName() {
|
||||
return allEngines().find((engine) => engine.type === this.model.type)?.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,21 +74,21 @@ export default class AwsRootConfig extends Model {
|
||||
return formFields.filter((attr) => attr.name !== 'secretKey');
|
||||
}
|
||||
|
||||
// "filedGroupsWif" and "fieldGroupsIam" are passed to the FormFieldGroups component to determine which group to show in the form (ex: @groupName="fieldGroupsWif")
|
||||
// "filedGroupsWif" and "fieldGroupsAccount" are passed to the FormFieldGroups component to determine which group to show in the form (ex: @groupName="fieldGroupsWif")
|
||||
get fieldGroupsWif() {
|
||||
return fieldToAttrs(this, this.formFieldGroups('wif'));
|
||||
}
|
||||
|
||||
get fieldGroupsIam() {
|
||||
return fieldToAttrs(this, this.formFieldGroups('iam'));
|
||||
get fieldGroupsAccount() {
|
||||
return fieldToAttrs(this, this.formFieldGroups('account'));
|
||||
}
|
||||
|
||||
formFieldGroups(accessType = 'iam') {
|
||||
formFieldGroups(accessType = 'account') {
|
||||
const formFieldGroups = [];
|
||||
if (accessType === 'wif') {
|
||||
formFieldGroups.push({ default: ['roleArn', 'identityTokenAudience', 'identityTokenTtl'] });
|
||||
}
|
||||
if (accessType === 'iam') {
|
||||
if (accessType === 'account') {
|
||||
formFieldGroups.push({ default: ['accessKey', 'secretKey'] });
|
||||
}
|
||||
formFieldGroups.push({
|
||||
|
||||
@@ -64,7 +64,7 @@ export default class AzureConfig extends Model {
|
||||
return !!this.identityTokenAudience || !!this.identityTokenTtl;
|
||||
}
|
||||
|
||||
get isAzureAccountConfigured() {
|
||||
get isAccountPluginConfigured() {
|
||||
// clientSecret is not checked here because it's never return by the API
|
||||
// however it is an Azure account field
|
||||
return !!this.rootPasswordTtl;
|
||||
@@ -79,16 +79,16 @@ export default class AzureConfig extends Model {
|
||||
return formFields.filter((attr) => attr.name !== 'clientSecret');
|
||||
}
|
||||
|
||||
// "filedGroupsWif" and "fieldGroupsAzure" are passed to the FormFieldGroups component to determine which group to show in the form (ex: @groupName="fieldGroupsWif")
|
||||
// "filedGroupsWif" and "fieldGroupsAccount" are passed to the FormFieldGroups component to determine which group to show in the form (ex: @groupName="fieldGroupsWif")
|
||||
get fieldGroupsWif() {
|
||||
return fieldToAttrs(this, this.formFieldGroups('wif'));
|
||||
}
|
||||
|
||||
get fieldGroupsAzure() {
|
||||
return fieldToAttrs(this, this.formFieldGroups('azure'));
|
||||
get fieldGroupsAccount() {
|
||||
return fieldToAttrs(this, this.formFieldGroups('account'));
|
||||
}
|
||||
|
||||
formFieldGroups(accessType = 'azure') {
|
||||
formFieldGroups(accessType = 'account') {
|
||||
const formFieldGroups = [];
|
||||
formFieldGroups.push({
|
||||
default: ['subscriptionId', 'tenantId', 'clientId', 'environment'],
|
||||
@@ -98,7 +98,7 @@ export default class AzureConfig extends Model {
|
||||
default: ['identityTokenAudience', 'identityTokenTtl'],
|
||||
});
|
||||
}
|
||||
if (accessType === 'azure') {
|
||||
if (accessType === 'account') {
|
||||
formFieldGroups.push({
|
||||
default: ['clientSecret', 'rootPasswordTtl'],
|
||||
});
|
||||
|
||||
@@ -19,8 +19,8 @@ import type VersionService from 'vault/services/version';
|
||||
// It generates config models based on the engine type.
|
||||
// Saving and updating of those models are done within the engine specific components.
|
||||
|
||||
const CONFIG_ADAPTERS_PATHS: Record<string, string[]> = {
|
||||
aws: ['aws/lease-config', 'aws/root-config'],
|
||||
const MOUNT_CONFIG_MODEL_NAMES: Record<string, string[]> = {
|
||||
aws: ['aws/root-config', 'aws/lease-config'],
|
||||
azure: ['azure/config'],
|
||||
ssh: ['ssh/ca-config'],
|
||||
};
|
||||
@@ -29,6 +29,19 @@ export default class SecretsBackendConfigurationEdit extends Route {
|
||||
@service declare readonly store: Store;
|
||||
@service declare readonly version: VersionService;
|
||||
|
||||
standardizedModelName(type: string, modelName: string) {
|
||||
// to determine if there is an additional config model, we check if the modelName is the same as the second element in the array.
|
||||
const path =
|
||||
MOUNT_CONFIG_MODEL_NAMES[type] && MOUNT_CONFIG_MODEL_NAMES[type].length > 1
|
||||
? MOUNT_CONFIG_MODEL_NAMES[type][1]
|
||||
: null;
|
||||
if (modelName === path) {
|
||||
return 'additional-config-model';
|
||||
} else {
|
||||
return 'mount-config-model';
|
||||
}
|
||||
}
|
||||
|
||||
async model() {
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
const secretEngineRecord = this.modelFor('vault.cluster.secrets.backend') as SecretEngineModel;
|
||||
@@ -43,12 +56,12 @@ export default class SecretsBackendConfigurationEdit extends Route {
|
||||
// generate the model based on the engine type.
|
||||
// and pre-set model with type and backend e.g. {type: ssh, id: ssh-123}
|
||||
const model: Record<string, unknown> = { type, id: backend };
|
||||
for (const adapterPath of CONFIG_ADAPTERS_PATHS[type] as string[]) {
|
||||
// convert the adapterPath with a name that can be passed to the components
|
||||
// ex: adapterPath = ssh/ca-config, convert to: ssh-ca-config so that you can pass to component @model={{this.model.ssh-ca-config}}
|
||||
const standardizedKey = adapterPath.replace(/\//g, '-');
|
||||
for (const modelName of MOUNT_CONFIG_MODEL_NAMES[type] as string[]) {
|
||||
// create a key that corresponds with the model order
|
||||
// ex: modelName = aws/lease-config, convert to: additional-config-model so that you can pass to component @additionalConfigModel={{this.model.additional-config-model}}
|
||||
const standardizedKey = this.standardizedModelName(type, modelName);
|
||||
try {
|
||||
const configModel = await this.store.queryRecord(adapterPath, {
|
||||
const configModel = await this.store.queryRecord(modelName, {
|
||||
backend,
|
||||
type,
|
||||
});
|
||||
@@ -56,7 +69,7 @@ export default class SecretsBackendConfigurationEdit extends Route {
|
||||
// so instead of checking a catch or httpStatus, we check if the model is configured based on the getter `isConfigured` on the engine's model
|
||||
// if the engine is not configured we update the record to get the default values
|
||||
if (!configModel.isConfigured && type === 'azure') {
|
||||
model[standardizedKey] = await this.store.createRecord(adapterPath, {
|
||||
model[standardizedKey] = await this.store.createRecord(modelName, {
|
||||
backend,
|
||||
type,
|
||||
});
|
||||
@@ -71,7 +84,7 @@ export default class SecretsBackendConfigurationEdit extends Route {
|
||||
e.httpStatus === 404 ||
|
||||
(type === 'ssh' && e.httpStatus === 400 && errorMessage(e) === `keys haven't been configured yet`)
|
||||
) {
|
||||
model[standardizedKey] = await this.store.createRecord(adapterPath, {
|
||||
model[standardizedKey] = await this.store.createRecord(modelName, {
|
||||
backend,
|
||||
type,
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-backend-configure-title={{this.model.type}}>
|
||||
Configure
|
||||
{{get (options-for-backend this.model.type) "displayName"}}
|
||||
{{this.displayName}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
@@ -28,19 +28,16 @@
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
{{#if (eq this.model.type "aws")}}
|
||||
<SecretEngine::ConfigureAws
|
||||
@leaseConfig={{this.model.aws-lease-config}}
|
||||
@rootConfig={{this.model.aws-root-config}}
|
||||
@issuerConfig={{this.model.identity-oidc-config}}
|
||||
@backendPath={{this.model.id}}
|
||||
/>
|
||||
{{else if (eq this.model.type "azure")}}
|
||||
<SecretEngine::ConfigureAzure
|
||||
@model={{this.model.azure-config}}
|
||||
{{#if (eq this.model.type "ssh")}}
|
||||
<SecretEngine::ConfigureSsh @model={{this.model.mount-config-model}} @id={{this.model.id}} />
|
||||
{{! This "else if" check is preventive. As of writing, all engines using this route, but "ssh", are wif engines }}
|
||||
{{else if this.isWifEngine}}
|
||||
<SecretEngine::ConfigureWif
|
||||
@backendPath={{this.model.id}}
|
||||
@displayName={{this.displayName}}
|
||||
@type={{this.model.type}}
|
||||
@mountConfigModel={{this.model.mount-config-model}}
|
||||
@additionalConfigModel={{this.model.additional-config-model}}
|
||||
@issuerConfig={{this.model.identity-oidc-config}}
|
||||
/>
|
||||
{{else if (eq this.model.type "ssh")}}
|
||||
<SecretEngine::ConfigureSsh @model={{this.model.ssh-ca-config}} @id={{this.model.id}} />
|
||||
{{/if}}
|
||||
@@ -34,10 +34,12 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.flashSuccessSpy = spy(flash, 'success');
|
||||
this.flashInfoSpy = spy(flash, 'info');
|
||||
this.flashDangerSpy = spy(flash, 'danger');
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.uid = uuidv4();
|
||||
return authPage.login();
|
||||
});
|
||||
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
@@ -62,7 +64,13 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
await click(SES.configure);
|
||||
assert.strictEqual(currentURL(), `/vault/secrets/${path}/configuration/edit`);
|
||||
assert.dom(SES.configureTitle('aws')).hasText('Configure AWS');
|
||||
assert.dom(SES.aws.rootForm).exists('it lands on the root configuration form.');
|
||||
assert.dom(SES.configureForm).exists('it lands on the configuration form.');
|
||||
assert
|
||||
.dom(SES.additionalConfigModelTitle)
|
||||
.hasText(
|
||||
'Lease Configuration',
|
||||
'it shows the lease configuration section with the "Lease Configuration" title.'
|
||||
);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
@@ -83,9 +91,8 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
await enablePage.enable('aws', path);
|
||||
|
||||
this.server.post(configUrl('aws-lease', path), () => {
|
||||
assert.false(
|
||||
true,
|
||||
'post request was made to config/lease when no data was changed. test should fail.'
|
||||
throw new Error(
|
||||
'A POST request was made to config/lease when it should not because no data was changed.'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -93,13 +100,13 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig('withWif');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningModal).exists('issue warning modal exists');
|
||||
assert.dom(SES.wif.issuerWarningModal).exists('issuer warning modal exists');
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
// three flash messages, the first is about mounting the engine, only care about the last two
|
||||
assert.strictEqual(
|
||||
this.flashSuccessSpy.args[1][0],
|
||||
`Successfully saved ${path}'s root configuration.`,
|
||||
'first flash message about the root config.'
|
||||
`Successfully saved ${path}'s configuration.`,
|
||||
'first flash message about the first model config.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
this.flashSuccessSpy.args[2][0],
|
||||
@@ -124,11 +131,11 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
const type = 'aws';
|
||||
this.server.get(`${path}/config/root`, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.ok(true, 'request made to config/root when navigating to the configuration page.');
|
||||
assert.true(true, 'request made to config/root when navigating to the configuration page.');
|
||||
return { data: { id: path, type, attributes: payload } };
|
||||
});
|
||||
this.server.get(`identity/oidc/config`, () => {
|
||||
assert.false(true, 'request made to return issuer. test should fail.');
|
||||
throw new Error(`Request was made to return the issuer when it should not have been.`);
|
||||
});
|
||||
await enablePage.enable(type, path);
|
||||
createConfig(this.store, path, type); // create the aws root config in the store
|
||||
@@ -145,10 +152,7 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
await enablePage.enable('aws', path);
|
||||
|
||||
this.server.post(configUrl('aws-lease', path), () => {
|
||||
assert.false(
|
||||
true,
|
||||
'post request was made to config/lease when no data was changed. test should fail.'
|
||||
);
|
||||
throw new Error(`post request was made to config/lease when it should not have been.`);
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
@@ -156,8 +160,8 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
await fillInAwsConfig('withAccess');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s root configuration.`),
|
||||
'Success flash message is rendered showing the root configuration was saved.'
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s configuration.`),
|
||||
'Success flash message is rendered showing the configuration was saved.'
|
||||
);
|
||||
assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', 'Access Key has been set.');
|
||||
assert
|
||||
@@ -190,40 +194,12 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should save lease AWS configuration', async function (assert) {
|
||||
assert.expect(3);
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
|
||||
this.server.post(configUrl('aws', path), () => {
|
||||
assert.false(
|
||||
true,
|
||||
'post request was made to config/root when no data was changed. test should fail.'
|
||||
);
|
||||
});
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s lease configuration.`),
|
||||
'Success flash message is rendered showing the lease configuration was saved.'
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Default Lease TTL'))
|
||||
.hasText('33 seconds', `Default TTL has been set.`);
|
||||
assert.dom(GENERAL.infoRowValue('Max Lease TTL')).hasText('44 seconds', `Max lease TTL has been set.`);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it shows AWS mount configuration details', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
this.server.get(`${path}/config/root`, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.ok(true, 'request made to config/root when navigating to the configuration page.');
|
||||
assert.true(true, 'request made to config/root when navigating to the configuration page.');
|
||||
return { data: { id: path, type, attributes: payload } };
|
||||
});
|
||||
await enablePage.enable(type, path);
|
||||
@@ -280,19 +256,6 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should show API error when AWS configuration read fails', async function (assert) {
|
||||
assert.expect(1);
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
await enablePage.enable(type, path);
|
||||
// interrupt get and return API error
|
||||
this.server.get(configUrl(type, path), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
await click(SES.configTab);
|
||||
assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route');
|
||||
});
|
||||
|
||||
test('it should not make a post request if lease or root data was unchanged', async function (assert) {
|
||||
assert.expect(3);
|
||||
const path = `aws-${this.uid}`;
|
||||
@@ -300,16 +263,10 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
await enablePage.enable(type, path);
|
||||
|
||||
this.server.post(configUrl(type, path), () => {
|
||||
assert.false(
|
||||
true,
|
||||
'post request was made to config/root when no data was changed. test should fail.'
|
||||
);
|
||||
throw new Error(`post request was made to config/root when it should not have been.`);
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', path), () => {
|
||||
assert.false(
|
||||
true,
|
||||
'post request was made to config/lease when no data was changed. test should fail.'
|
||||
);
|
||||
throw new Error(`post request was made to config/lease when it should not have been.`);
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
@@ -350,6 +307,35 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it saves lease configuration if root configuration was not changed', async function (assert) {
|
||||
assert.expect(2);
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
|
||||
this.server.post(configUrl('aws', path), () => {
|
||||
throw new Error(
|
||||
`Request was made to save the config/root when it should not have been because the user did not make any changes to this config.`
|
||||
);
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.saveButton);
|
||||
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s lease configuration.`),
|
||||
'Success flash message is rendered showing the lease configuration was saved.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/configuration`,
|
||||
'the form transitioned as expected to the details page'
|
||||
);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
});
|
||||
|
||||
module('isCommunity', function (hooks) {
|
||||
@@ -377,5 +363,97 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
module('Error handling', function () {
|
||||
test('it does not try to save lease configuration if root configuration errored on save', async function (assert) {
|
||||
assert.expect(1);
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
|
||||
this.server.post(configUrl('aws', path), () => {
|
||||
assert.true(true, 'post request was made to save aws root config.');
|
||||
return overrideResponse(400, { errors: ['bad request!'] });
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', path), () => {
|
||||
throw new Error(
|
||||
`post request was made to config/lease when the first config was not saved. A request to this endpoint should NOT be be made`
|
||||
);
|
||||
});
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig('withAccess');
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.saveButton);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it shows a flash message error and transitions if lease configuration errored on save', async function (assert) {
|
||||
assert.expect(2);
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
|
||||
this.server.post(configUrl('aws', path), () => {
|
||||
throw new Error(
|
||||
`Request was made to save the config/root when it should not have been because the user did not make any changes to this config.`
|
||||
);
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', path), () => {
|
||||
return overrideResponse(400, { errors: ['bad request!'] });
|
||||
});
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.saveButton);
|
||||
|
||||
assert.true(
|
||||
this.flashDangerSpy.calledWith(`Lease configuration was not saved: bad request!`),
|
||||
'flash danger message is rendered showing the lease configuration was NOT saved.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/configuration`,
|
||||
'lease configuration failed to save but the component transitioned as expected'
|
||||
);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it prevents transition and shows api error if root config errored on save', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
|
||||
this.server.post(configUrl('aws', path), () => {
|
||||
return overrideResponse(400, { errors: ['welp, that did not work!'] });
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig('withAccess');
|
||||
await click(GENERAL.saveButton);
|
||||
|
||||
assert.dom(GENERAL.messageError).hasText('Error welp, that did not work!', 'API error shows on form');
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/configuration/edit`,
|
||||
'the form did not transition because the save failed.'
|
||||
);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should show API error when AWS configuration read fails', async function (assert) {
|
||||
assert.expect(1);
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
await enablePage.enable(type, path);
|
||||
// interrupt get and return API error
|
||||
this.server.get(configUrl(type, path), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
await click(SES.configTab);
|
||||
assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
||||
environment: 'AZUREPUBLICCLOUD',
|
||||
};
|
||||
this.server.get(`${path}/config`, () => {
|
||||
assert.ok(true, 'request made to config when navigating to the configuration page.');
|
||||
assert.true(true, 'request made to config when navigating to the configuration page.');
|
||||
return { data: { id: path, type: this.type, ...azureAccountAttrs } };
|
||||
});
|
||||
await enablePage.enable(this.type, path);
|
||||
@@ -109,17 +109,6 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should show API error when configuration read fails', async function (assert) {
|
||||
assert.expect(1);
|
||||
const path = `azure-${this.uid}`;
|
||||
// interrupt get and return API error
|
||||
this.server.get(configUrl(this.type, path), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
await enablePage.enable(this.type, path);
|
||||
assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route');
|
||||
});
|
||||
});
|
||||
|
||||
module('create', function () {
|
||||
@@ -129,9 +118,8 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
||||
await enablePage.enable(this.type, path);
|
||||
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
assert.notOk(
|
||||
true,
|
||||
'post request was made to issuer endpoint when on community and data not changed. test should fail.'
|
||||
throw new Error(
|
||||
`Request was made to return the issuer when it should not have been because user is on CE.`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -226,6 +214,42 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
});
|
||||
|
||||
module('Error handling', function () {
|
||||
test('it prevents transition and shows api error if config errored on save', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
await enablePage.enable('azure', path);
|
||||
|
||||
this.server.post(configUrl('azure', path), () => {
|
||||
return overrideResponse(400, { errors: ['welp, that did not work!'] });
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAzureConfig('azure');
|
||||
await click(GENERAL.saveButton);
|
||||
|
||||
assert.dom(GENERAL.messageError).hasText('Error welp, that did not work!', 'API error shows on form');
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/configuration/edit`,
|
||||
'the form did not transition because the save failed.'
|
||||
);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should show API error when configuration read fails', async function (assert) {
|
||||
assert.expect(1);
|
||||
const path = `azure-${this.uid}`;
|
||||
// interrupt get and return API error
|
||||
this.server.get(configUrl(this.type, path), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
await enablePage.enable(this.type, path);
|
||||
assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module('isEnterprise', function (hooks) {
|
||||
@@ -245,7 +269,7 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
||||
environment: 'AZUREPUBLICCLOUD',
|
||||
};
|
||||
this.server.get(`${path}/config`, () => {
|
||||
assert.ok(true, 'request made to config when navigating to the configuration page.');
|
||||
assert.true(true, 'request made to config when navigating to the configuration page.');
|
||||
return { data: { id: path, type: this.type, ...wifAttrs } };
|
||||
});
|
||||
await enablePage.enable(this.type, path);
|
||||
@@ -268,11 +292,11 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
||||
const path = `azure-${this.uid}`;
|
||||
this.server.get(`${path}/config`, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.ok(true, 'request made to config/root when navigating to the configuration page.');
|
||||
assert.true(true, 'request made to config/root when navigating to the configuration page.');
|
||||
return { data: { id: path, type: this.type, attributes: payload } };
|
||||
});
|
||||
this.server.get(`identity/oidc/config`, () => {
|
||||
assert.notOk(true, 'request made to return issuer. test should fail.');
|
||||
throw new Error(`Request was made to return the issuer when it should not have been.`);
|
||||
});
|
||||
await createConfig(this.store, path, this.type); // create the azure account config in the store
|
||||
await enablePage.enable(this.type, path);
|
||||
|
||||
@@ -72,7 +72,7 @@ module('Acceptance | GCP | configuration', function (hooks) {
|
||||
ttl: 3600,
|
||||
};
|
||||
this.server.get(`${path}/config`, () => {
|
||||
assert.ok(true, 'request made to config when navigating to the configuration page.');
|
||||
assert.true(true, 'request made to config when navigating to the configuration page.');
|
||||
return { data: { id: path, type: this.type, ...wifAttrs } };
|
||||
});
|
||||
await enablePage.enable(this.type, path);
|
||||
@@ -99,7 +99,7 @@ module('Acceptance | GCP | configuration', function (hooks) {
|
||||
max_ttl: '4 hours',
|
||||
};
|
||||
this.server.get(`${path}/config`, () => {
|
||||
assert.ok(true, 'request made to config when navigating to the configuration page.');
|
||||
assert.true(true, 'request made to config when navigating to the configuration page.');
|
||||
return { data: { id: path, type: this.type, ...GCPAccountAttrs } };
|
||||
});
|
||||
await enablePage.enable(this.type, path);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export const SECRET_ENGINE_SELECTORS = {
|
||||
configTab: '[data-test-configuration-tab]',
|
||||
configure: '[data-test-secret-backend-configure]',
|
||||
configureNote: (name: string) => `[data-test-configure-note="${name}"]`,
|
||||
configureTitle: (type: string) => `[data-test-backend-configure-title="${type}"]`,
|
||||
configurationToggle: '[data-test-mount-config-toggle]',
|
||||
createSecret: '[data-test-secret-create]',
|
||||
@@ -24,6 +25,7 @@ export const SECRET_ENGINE_SELECTORS = {
|
||||
viewBackend: '[data-test-backend-view-link]',
|
||||
warning: '[data-test-warning]',
|
||||
configureForm: '[data-test-configure-form]',
|
||||
additionalConfigModelTitle: '[data-test-additional-config-model-title]',
|
||||
wif: {
|
||||
accessTypeSection: '[data-test-access-type-section]',
|
||||
accessTitle: '[data-test-access-title]',
|
||||
@@ -35,8 +37,6 @@ export const SECRET_ENGINE_SELECTORS = {
|
||||
issuerWarningSave: '[data-test-issuer-save]',
|
||||
},
|
||||
aws: {
|
||||
rootForm: '[data-test-root-form]',
|
||||
leaseTitle: '[data-test-lease-title]',
|
||||
deleteRole: (role: string) => `[data-test-aws-role-delete="${role}"]`,
|
||||
},
|
||||
ssh: {
|
||||
|
||||
@@ -1,578 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import sinon from 'sinon';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import { render, click, fillIn } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
import {
|
||||
expectedConfigKeys,
|
||||
createConfig,
|
||||
configUrl,
|
||||
fillInAwsConfig,
|
||||
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
import { capabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
|
||||
module('Integration | Component | SecretEngine/ConfigureAws', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.flashMessages = this.owner.lookup('service:flash-messages');
|
||||
this.flashMessages.registerTypes(['success', 'danger']);
|
||||
this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success');
|
||||
this.flashDangerSpy = sinon.spy(this.flashMessages, 'danger');
|
||||
this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
|
||||
this.uid = uuidv4();
|
||||
this.id = `aws-${this.uid}`;
|
||||
// using createRecord on root and lease configs to simulate a fresh mount
|
||||
this.rootConfig = this.store.createRecord('aws/root-config');
|
||||
this.leaseConfig = this.store.createRecord('aws/lease-config');
|
||||
// issuer config is never a createdRecord but the response from the API.
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
// Add backend to the configs because it's not on the testing snapshot (would come from url)
|
||||
this.rootConfig.backend = this.leaseConfig.backend = this.id;
|
||||
this.version = this.owner.lookup('service:version');
|
||||
// stub capabilities so that by default user can read and update issuer
|
||||
this.server.post('/sys/capabilities-self', () => capabilitiesStub('identity/oidc/config', ['sudo']));
|
||||
|
||||
this.renderComponent = () => {
|
||||
return render(hbs`
|
||||
<SecretEngine::ConfigureAws @rootConfig={{this.rootConfig}} @leaseConfig={{this.leaseConfig}} @issuerConfig={{this.issuerConfig}} @backendPath={{this.id}} />
|
||||
`);
|
||||
};
|
||||
});
|
||||
module('Create view', function () {
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
test('it renders fields ', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.aws.rootForm).exists('it lands on the aws root configuration form.');
|
||||
assert.dom(SES.wif.accessTitle).exists('Access section is rendered');
|
||||
assert.dom(SES.aws.leaseTitle).exists('Lease section is rendered');
|
||||
assert.dom(SES.wif.accessTypeSection).exists('Access type section is rendered');
|
||||
assert.dom(SES.wif.accessType('iam')).isChecked('defaults to showing IAM access type checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isNotChecked('wif access type is not checked');
|
||||
// check all the form fields are present
|
||||
await click(GENERAL.toggleGroup('Root config options'));
|
||||
for (const key of expectedConfigKeys('aws', true)) {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section.`);
|
||||
}
|
||||
for (const key of expectedConfigKeys('aws-lease')) {
|
||||
assert.dom(`[data-test-ttl-form-label="${key}"]`).exists(`${key} shows for Lease section.`);
|
||||
}
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders wif fields when selected', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
// check for the wif fields only
|
||||
for (const key of expectedConfigKeys('aws-wif', true)) {
|
||||
if (key === 'Identity token TTL') {
|
||||
assert.dom(GENERAL.ttl.toggle(key)).exists(`${key} shows for wif section.`);
|
||||
} else {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for wif section.`);
|
||||
}
|
||||
}
|
||||
// check iam fields do not show
|
||||
for (const key of expectedConfigKeys('aws', true)) {
|
||||
assert.dom(GENERAL.inputByAttr(key)).doesNotExist(`${key} does not show when wif is selected.`);
|
||||
}
|
||||
});
|
||||
|
||||
test('it clears wif/iam inputs after toggling accessType', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await fillInAwsConfig('withAccess');
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(SES.wif.accessType('wif')); // toggle to wif
|
||||
await fillInAwsConfig('withWif');
|
||||
await click(SES.wif.accessType('iam')); // toggle to wif
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('accessKey'))
|
||||
.hasValue('', 'accessKey is cleared after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('secretKey'))
|
||||
.hasValue('', 'secretKey is cleared after toggling accessType');
|
||||
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue('', 'issue shows no value after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasAttribute(
|
||||
'placeholder',
|
||||
'https://vault-test.com',
|
||||
'issue shows no value after toggling accessType'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('roleArn'))
|
||||
.hasValue('', 'roleArn is cleared after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('identityTokenAudience'))
|
||||
.hasValue('', 'identityTokenAudience is cleared after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.toggleInput('Identity token TTL'))
|
||||
.isNotChecked('identityTokenTtl is cleared after toggling accessType');
|
||||
});
|
||||
|
||||
test('it does not clear global issuer when toggling accessType', async function (assert) {
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(this.issuerConfig.issuer, 'issuer is what is sent in my the model on first load');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://ive-changed');
|
||||
await click(SES.wif.accessType('iam'));
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(
|
||||
this.issuerConfig.issuer,
|
||||
'issuer value is still the same global value after toggling accessType'
|
||||
);
|
||||
});
|
||||
|
||||
test('it shows validation error if default lease is entered but max lease is not', async function (assert) {
|
||||
assert.expect(2);
|
||||
await this.renderComponent();
|
||||
this.server.post(configUrl('aws-lease', this.id), () => {
|
||||
assert.false(
|
||||
true,
|
||||
'post request was made to config/lease when no data was changed. test should fail.'
|
||||
);
|
||||
});
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
assert.false(
|
||||
true,
|
||||
'post request was made to config/root when no data was changed. test should fail.'
|
||||
);
|
||||
});
|
||||
await click(GENERAL.ttl.toggle('Default Lease TTL'));
|
||||
await fillIn(GENERAL.ttl.input('Default Lease TTL'), '33');
|
||||
await click(GENERAL.saveButton);
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText('Lease TTL and Max Lease TTL are both required if one of them is set.');
|
||||
assert.dom(SES.aws.rootForm).exists('remains on the configuration form');
|
||||
});
|
||||
|
||||
test('it surfaces the API error if one occurs on root/config, preventing user from transitioning', async function (assert) {
|
||||
assert.expect(3);
|
||||
await this.renderComponent();
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', this.id), () => {
|
||||
assert.true(
|
||||
true,
|
||||
'post request was made to config/lease when config/root failed. test should pass.'
|
||||
);
|
||||
});
|
||||
// fill in both lease and root endpoints to ensure that both payloads are attempted to be sent
|
||||
await fillInAwsConfig('withAccess');
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(GENERAL.messageError).exists('API error surfaced to user');
|
||||
assert.dom(GENERAL.inlineError).exists('User shown inline error message');
|
||||
});
|
||||
|
||||
test('it allows user to submit root config even if API error occurs on config/lease config', async function (assert) {
|
||||
assert.expect(3);
|
||||
await this.renderComponent();
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
assert.true(
|
||||
true,
|
||||
'post request was made to config/root when config/lease failed. test should pass.'
|
||||
);
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', this.id), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
// fill in both lease and root endpoints to ensure that both payloads are attempted to be sent
|
||||
await fillInAwsConfig('withAccess');
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.saveButton);
|
||||
|
||||
assert.true(
|
||||
this.flashDangerSpy.calledWith('Lease configuration was not saved: bad request'),
|
||||
'Flash message shows that lease was not saved.'
|
||||
);
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it allows user to submit root config even if API error occurs on issuer config', async function (assert) {
|
||||
assert.expect(4);
|
||||
await this.renderComponent();
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
assert.true(true, 'post request was made to config/root when issuer failed. test should pass.');
|
||||
});
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
await fillInAwsConfig('withWif');
|
||||
await click(GENERAL.saveButton);
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
|
||||
assert.true(
|
||||
this.flashDangerSpy.calledWith('Issuer was not saved: bad request'),
|
||||
'Flash message shows that issuer was not saved'
|
||||
);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s root configuration.`),
|
||||
'Flash message shows that root was saved even if issuer was not'
|
||||
);
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it transitions without sending a lease, root, or issuer payload on cancel', async function (assert) {
|
||||
assert.expect(3);
|
||||
await this.renderComponent();
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
assert.true(
|
||||
false,
|
||||
'post request was made to config/root when user canceled out of flow. test should fail.'
|
||||
);
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', this.id), () => {
|
||||
assert.true(
|
||||
false,
|
||||
'post request was made to config/lease when user canceled out of flow. test should fail.'
|
||||
);
|
||||
});
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
assert.true(
|
||||
false,
|
||||
'post request was made to save issuer when user canceled out of flow. test should fail.'
|
||||
);
|
||||
});
|
||||
// fill in both lease and root endpoints to ensure that both payloads are attempted to be sent
|
||||
await fillInAwsConfig('withWif');
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.cancelButton);
|
||||
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.');
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
module('issuer field tests', function () {
|
||||
// the other tests where issuer is not passed do not show modals, so we only need to test when the modal should shows up
|
||||
test('if issuer API error and user changes issuer value, shows specific warning message', async function (assert) {
|
||||
this.issuerConfig.queryIssuerError = true;
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://change.me.no.read');
|
||||
await click(GENERAL.saveButton);
|
||||
assert
|
||||
.dom(SES.wif.issuerWarningMessage)
|
||||
.hasText(
|
||||
`You are updating the global issuer config. This will overwrite Vault's current issuer if it exists and may affect other configurations using this value. Continue?`,
|
||||
'modal shows message about overwriting value if it exists'
|
||||
);
|
||||
});
|
||||
|
||||
test('is shows placeholder issuer, shows modal when saving changes, and does not call APIs on cancel', async function (assert) {
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
assert.notOk(true, 'request should not be made to issuer config endpoint');
|
||||
});
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
assert.notOk(
|
||||
true,
|
||||
'post request was made to config/root when user canceled out of flow. test should fail.'
|
||||
);
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', this.id), () => {
|
||||
assert.notOk(
|
||||
true,
|
||||
'post request was made to config/lease when user canceled out of flow. test should fail.'
|
||||
);
|
||||
});
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasAttribute('placeholder', 'https://vault-test.com', 'shows issuer placeholder');
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'shows issuer is empty when not passed');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://bar.foo');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningModal).exists('issuer modal exists');
|
||||
assert
|
||||
.dom(SES.wif.issuerWarningMessage)
|
||||
.hasText(
|
||||
`You are updating the global issuer config. This will overwrite Vault's current issuer and may affect other configurations using this value. Continue?`,
|
||||
'modal shows message about overwriting value without the noRead: "if it exists" adage'
|
||||
);
|
||||
await click(SES.wif.issuerWarningCancel);
|
||||
assert.dom(SES.wif.issuerWarningModal).doesNotExist('issuer modal is removed on cancel');
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.');
|
||||
assert.true(this.transitionStub.notCalled, 'Does not redirect');
|
||||
});
|
||||
|
||||
test('it shows modal when updating issuer and calls correct APIs on save', async function (assert) {
|
||||
const newIssuer = 'http://bar.foo';
|
||||
this.server.post('/identity/oidc/config', (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(payload, { issuer: newIssuer }, 'payload for issuer is correct');
|
||||
return {
|
||||
id: 'identity-oidc-config', // id needs to match the id on secret-engine-helpers createIssuerConfig
|
||||
data: null,
|
||||
warnings: [
|
||||
'If "issuer" is set explicitly, all tokens must be validated against that address, including those issued by secondary clusters. Setting issuer to "" will restore the default behavior of using the cluster\'s api_addr as the issuer.',
|
||||
],
|
||||
};
|
||||
});
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
assert.notOk(true, 'skips request to config/root due to no changes');
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', this.id), () => {
|
||||
assert.notOk(true, 'skips request to config/lease due to no changes');
|
||||
});
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'issuer defaults to empty string');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), newIssuer);
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningModal).exists('issue warning modal exists');
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith('Issuer saved successfully'),
|
||||
'Success flash message called for issuer'
|
||||
);
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows modal when modifying the issuer, has correct payload, and shows flash message on fail', async function (assert) {
|
||||
assert.expect(7);
|
||||
this.issuer = 'http://foo.bar';
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
assert.true(
|
||||
true,
|
||||
'post request was made to config/root when unsetting the issuer. test should pass.'
|
||||
);
|
||||
});
|
||||
this.server.post('/identity/oidc/config', (_, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(payload, { issuer: this.issuer }, 'correctly sets the issuer');
|
||||
return overrideResponse(403);
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), this.issuer);
|
||||
await fillIn(GENERAL.inputByAttr('roleArn'), 'some-other-value');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningModal).exists('issuer warning modal exists');
|
||||
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
assert.true(
|
||||
this.flashDangerSpy.calledWith('Issuer was not saved: permission denied'),
|
||||
'shows danger flash for issuer save'
|
||||
);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s root configuration.`),
|
||||
"calls the root flash message not the issuer's"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
module('isCommunity', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'community';
|
||||
});
|
||||
test('it renders fields', async function (assert) {
|
||||
assert.expect(13);
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.aws.rootForm).exists('it lands on the aws root configuration form.');
|
||||
assert.dom(SES.wif.accessTitle).exists('Access section is rendered');
|
||||
assert.dom(SES.aws.leaseTitle).exists('Lease section is rendered');
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSection)
|
||||
.doesNotExist('Access type section does not render for a community user');
|
||||
// check all the form fields are present
|
||||
await click(GENERAL.toggleGroup('Root config options'));
|
||||
for (const key of expectedConfigKeys('aws', true)) {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section.`);
|
||||
}
|
||||
for (const key of expectedConfigKeys('aws-lease')) {
|
||||
assert.dom(`[data-test-ttl-form-label="${key}"]`).exists(`${key} shows for Lease section.`);
|
||||
}
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist();
|
||||
});
|
||||
test('it does not send issuer on save', async function (assert) {
|
||||
assert.expect(4);
|
||||
await this.renderComponent();
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
assert.true(true, 'post request was made to config/root. test should pass.');
|
||||
});
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
throw new Error('post request was incorrectly made to update issuer');
|
||||
});
|
||||
await fillInAwsConfig('withAccess');
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningModal).doesNotExist('modal should not render');
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s root configuration.`),
|
||||
'Flash message shows that root was saved even if issuer was not'
|
||||
);
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
module('Edit view', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.rootConfig = createConfig(this.store, this.id, 'aws');
|
||||
this.leaseConfig = createConfig(this.store, this.id, 'aws-lease');
|
||||
});
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
test('it defaults to IAM accessType if IAM fields are already set', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('iam')).isChecked('IAM accessType is checked');
|
||||
assert.dom(SES.wif.accessType('iam')).isDisabled('IAM accessType is disabled');
|
||||
assert.dom(SES.wif.accessType('wif')).isNotChecked('WIF accessType is not checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSubtext)
|
||||
.hasText('You cannot edit Access Type if you have already saved access credentials.');
|
||||
});
|
||||
|
||||
test('it defaults to WIF accessType if WIF fields are already set', async function (assert) {
|
||||
this.rootConfig = createConfig(this.store, this.id, 'aws-wif');
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert.dom(SES.wif.accessType('iam')).isNotChecked('IAM accessType is not checked');
|
||||
assert.dom(SES.wif.accessType('iam')).isDisabled('IAM accessType is disabled');
|
||||
assert.dom(GENERAL.inputByAttr('roleArn')).hasValue(this.rootConfig.roleArn);
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSubtext)
|
||||
.hasText('You cannot edit Access Type if you have already saved access credentials.');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('identityTokenAudience'))
|
||||
.hasValue(this.rootConfig.identityTokenAudience);
|
||||
assert.dom(GENERAL.ttl.input('Identity token TTL')).hasValue('2'); // 7200 on payload is 2hrs in ttl picker
|
||||
});
|
||||
|
||||
test('it renders issuer if global issuer is already set', async function (assert) {
|
||||
this.rootConfig = createConfig(this.store, this.id, 'aws-wif');
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(this.issuerConfig.issuer, 'it has the models issuer value');
|
||||
});
|
||||
|
||||
test('it allows you to change access type if record does not have wif or iam values already set', async function (assert) {
|
||||
// the model does not have to be new for a user to see the option to change the access type.
|
||||
// the access type is only disabled if the model has values already set for access type fields.
|
||||
this.rootConfig = createConfig(this.store, this.id, 'aws-no-access');
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled');
|
||||
assert.dom(SES.wif.accessType('iam')).isNotDisabled('IAM accessType is NOT disabled');
|
||||
});
|
||||
|
||||
test('it shows previously saved root and lease information', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inputByAttr('accessKey')).hasValue(this.rootConfig.accessKey);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('secretKey'))
|
||||
.hasValue('**********', 'secretKey is masked on edit the value');
|
||||
|
||||
await click(GENERAL.toggleGroup('Root config options'));
|
||||
assert.dom(GENERAL.inputByAttr('region')).hasValue(this.rootConfig.region);
|
||||
assert.dom(GENERAL.inputByAttr('iamEndpoint')).hasValue(this.rootConfig.iamEndpoint);
|
||||
assert.dom(GENERAL.inputByAttr('stsEndpoint')).hasValue(this.rootConfig.stsEndpoint);
|
||||
assert.dom(GENERAL.inputByAttr('maxRetries')).hasValue('1');
|
||||
// Check lease config values
|
||||
assert.dom(GENERAL.ttl.input('Default Lease TTL')).hasValue('50');
|
||||
assert.dom(GENERAL.ttl.input('Max Lease TTL')).hasValue('55');
|
||||
});
|
||||
|
||||
test('it requires a double click to change the secret key', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
this.server.post(configUrl('aws', this.id), (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(
|
||||
payload.secret_key,
|
||||
'new-secret',
|
||||
'post request was made to config/root with the updated secret_key.'
|
||||
);
|
||||
});
|
||||
|
||||
await click(GENERAL.enableField('secretKey'));
|
||||
await click('[data-test-button="toggle-masked"]');
|
||||
await fillIn(GENERAL.inputByAttr('secretKey'), 'new-secret');
|
||||
await click(GENERAL.saveButton);
|
||||
});
|
||||
});
|
||||
module('isCommunity', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'community';
|
||||
});
|
||||
|
||||
test('it does not show access types but defaults to iam fields', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessTypeSection).doesNotExist('Access type section does not render');
|
||||
assert.dom(GENERAL.inputByAttr('accessKey')).hasValue(this.rootConfig.accessKey);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('secretKey'))
|
||||
.hasValue('**********', 'secretKey is masked on edit the value');
|
||||
|
||||
await click(GENERAL.toggleGroup('Root config options'));
|
||||
assert.dom(GENERAL.inputByAttr('region')).hasValue(this.rootConfig.region);
|
||||
assert.dom(GENERAL.inputByAttr('iamEndpoint')).hasValue(this.rootConfig.iamEndpoint);
|
||||
assert.dom(GENERAL.inputByAttr('stsEndpoint')).hasValue(this.rootConfig.stsEndpoint);
|
||||
assert.dom(GENERAL.inputByAttr('maxRetries')).hasValue('1');
|
||||
// Check lease config values
|
||||
assert.dom(GENERAL.ttl.input('Default Lease TTL')).hasValue('50');
|
||||
assert.dom(GENERAL.ttl.input('Max Lease TTL')).hasValue('55');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,426 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import sinon from 'sinon';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import { render, click, fillIn } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
import {
|
||||
expectedConfigKeys,
|
||||
createConfig,
|
||||
configUrl,
|
||||
fillInAzureConfig,
|
||||
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
import { capabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
|
||||
module('Integration | Component | SecretEngine/ConfigureAzure', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.flashMessages = this.owner.lookup('service:flash-messages');
|
||||
this.flashMessages.registerTypes(['success', 'danger']);
|
||||
this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success');
|
||||
this.flashDangerSpy = sinon.spy(this.flashMessages, 'danger');
|
||||
this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
|
||||
this.uid = uuidv4();
|
||||
this.id = `azure-${this.uid}`;
|
||||
this.config = this.store.createRecord('azure/config');
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.config.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url)
|
||||
// stub capabilities so that by default user can read and update issuer
|
||||
this.server.post('/sys/capabilities-self', () => capabilitiesStub('identity/oidc/config', ['sudo']));
|
||||
|
||||
this.renderComponent = () => {
|
||||
return render(hbs`
|
||||
<SecretEngine::ConfigureAzure @model={{this.config}} @issuerConfig={{this.issuerConfig}} @backendPath={{this.id}} />
|
||||
`);
|
||||
};
|
||||
});
|
||||
module('Create view', function () {
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
test('it renders default fields, showing access type options for enterprise users', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.configureForm).exists('it lands on the Azure configuration form.');
|
||||
assert.dom(SES.wif.accessType('azure')).isChecked('defaults to showing Azure access type checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isNotChecked('wif access type is not checked');
|
||||
// check all the form fields are present
|
||||
for (const key of expectedConfigKeys('azure', true)) {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section`);
|
||||
}
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders wif fields when user selects wif access type', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
// check for the wif fields only
|
||||
for (const key of expectedConfigKeys('azure-wif', true)) {
|
||||
if (key === 'Identity token TTL') {
|
||||
assert.dom(GENERAL.ttl.toggle(key)).exists(`${key} shows for wif section.`);
|
||||
} else {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for wif section.`);
|
||||
}
|
||||
}
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).exists('issuer shows for wif section.');
|
||||
});
|
||||
|
||||
test('it clears wif/azure-account inputs after toggling accessType', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await fillInAzureConfig('azure');
|
||||
await click(SES.wif.accessType('wif'));
|
||||
await fillInAzureConfig('withWif');
|
||||
await click(SES.wif.accessType('azure'));
|
||||
|
||||
assert
|
||||
.dom(GENERAL.toggleInput('Root password TTL'))
|
||||
.isNotChecked('rootPasswordTtl is cleared after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('clientSecret'))
|
||||
.hasValue('', 'clientSecret is cleared after toggling accessType');
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue('', 'issuer shows no value after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasAttribute(
|
||||
'placeholder',
|
||||
'https://vault-test.com',
|
||||
'issuer shows no value after toggling accessType'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('identityTokenAudience'))
|
||||
.hasValue('', 'idTokenAudience is cleared after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.toggleInput('Identity token TTL'))
|
||||
.isNotChecked('identityTokenTtl is cleared after toggling accessType');
|
||||
});
|
||||
|
||||
test('it does not clear global issuer when toggling accessType', async function (assert) {
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(this.issuerConfig.issuer, 'issuer is what is sent in by the model on first load');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://ive-changed');
|
||||
await click(SES.wif.accessType('azure'));
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(
|
||||
this.issuerConfig.issuer,
|
||||
'issuer value is still the same global value after toggling accessType'
|
||||
);
|
||||
});
|
||||
|
||||
test('it transitions without sending a config or issuer payload on cancel', async function (assert) {
|
||||
assert.expect(3);
|
||||
await this.renderComponent();
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
assert.notOk(
|
||||
true,
|
||||
'post request was made to config when user canceled out of flow. test should fail.'
|
||||
);
|
||||
});
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
assert.notOk(
|
||||
true,
|
||||
'post request was made to save issuer when user canceled out of flow. test should fail.'
|
||||
);
|
||||
});
|
||||
await fillInAzureConfig('withWif');
|
||||
await click(GENERAL.cancelButton);
|
||||
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.');
|
||||
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
module('issuer field tests', function () {
|
||||
test('if issuer API error and user changes issuer value, shows specific warning message', async function (assert) {
|
||||
this.issuerConfig.queryIssuerError = true;
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://change.me.no.read');
|
||||
await click(GENERAL.saveButton);
|
||||
assert
|
||||
.dom(SES.wif.issuerWarningMessage)
|
||||
.hasText(
|
||||
`You are updating the global issuer config. This will overwrite Vault's current issuer if it exists and may affect other configurations using this value. Continue?`,
|
||||
'modal shows message about overwriting value if it exists'
|
||||
);
|
||||
});
|
||||
|
||||
test('is shows placeholder issuer, and does not call APIs on canceling out of issuer modal', async function (assert) {
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
assert.notOk(true, 'request should not be made to issuer config endpoint');
|
||||
});
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
assert.notOk(
|
||||
true,
|
||||
'post request was made to config/ when user canceled out of flow. test should fail.'
|
||||
);
|
||||
});
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasAttribute('placeholder', 'https://vault-test.com', 'shows issuer placeholder');
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'shows issuer is empty when not passed');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://bar.foo');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningMessage).exists('issuer modal exists');
|
||||
assert
|
||||
.dom(SES.wif.issuerWarningMessage)
|
||||
.hasText(
|
||||
`You are updating the global issuer config. This will overwrite Vault's current issuer and may affect other configurations using this value. Continue?`,
|
||||
'modal shows message about overwriting value without the noRead: "if it exists" adage'
|
||||
);
|
||||
await click(SES.wif.issuerWarningCancel);
|
||||
assert.dom(SES.wif.issuerWarningMessage).doesNotExist('issuer modal is removed on cancel');
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.');
|
||||
assert.true(this.transitionStub.notCalled, 'Does not redirect');
|
||||
});
|
||||
|
||||
test('it shows modal when updating issuer and calls correct APIs on save', async function (assert) {
|
||||
const newIssuer = `http://bar.${uuidv4()}`;
|
||||
this.server.post('/identity/oidc/config', (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(payload, { issuer: newIssuer }, 'payload for issuer is correct');
|
||||
return {
|
||||
id: 'identity-oidc-config', // id needs to match the id on secret-engine-helpers createIssuerConfig
|
||||
data: null,
|
||||
warnings: [
|
||||
'If "issuer" is set explicitly, all tokens must be validated against that address, including those issued by secondary clusters. Setting issuer to "" will restore the default behavior of using the cluster\'s api_addr as the issuer.',
|
||||
],
|
||||
};
|
||||
});
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
assert.notOk(true, 'skips request to config because the model was not changed');
|
||||
});
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'issuer defaults to empty string');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), newIssuer);
|
||||
await click(GENERAL.saveButton);
|
||||
|
||||
assert.dom(SES.wif.issuerWarningMessage).exists('issue warning modal exists');
|
||||
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith('Issuer saved successfully'),
|
||||
'Success flash message called for Azure issuer'
|
||||
);
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows modal when modifying the issuer, has correct payload, and shows flash message on fail', async function (assert) {
|
||||
assert.expect(7);
|
||||
this.issuer = 'http://foo.bar';
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
assert.true(
|
||||
true,
|
||||
'post request was made to azure config when unsetting the issuer. test should pass.'
|
||||
);
|
||||
});
|
||||
this.server.post('/identity/oidc/config', (_, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(payload, { issuer: this.issuer }, 'correctly sets the issuer');
|
||||
return overrideResponse(403);
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), this.issuer);
|
||||
await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'some-value');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningMessage).exists('issuer warning modal exists');
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
|
||||
assert.true(
|
||||
this.flashDangerSpy.calledWith('Issuer was not saved: permission denied'),
|
||||
'shows danger flash for issuer save'
|
||||
);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`),
|
||||
"calls the config flash message not the issuer's"
|
||||
);
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
module('isCommunity', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'community';
|
||||
});
|
||||
|
||||
test('it renders fields', async function (assert) {
|
||||
assert.expect(9);
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.configureForm).exists('t lands on the Azure configuration form');
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSection)
|
||||
.doesNotExist('Access type section does not render for a community user');
|
||||
// check all the form fields are present
|
||||
for (const key of expectedConfigKeys('azure', true)) {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for azure account creds section.`);
|
||||
}
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist();
|
||||
});
|
||||
|
||||
test('it does not send issuer on save', async function (assert) {
|
||||
assert.expect(4);
|
||||
await this.renderComponent();
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
assert.true(true, 'post request was made to config. test should pass.');
|
||||
});
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
throw new Error('post request was incorrectly made to update issuer');
|
||||
});
|
||||
await fillInAzureConfig('azure');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningMessage).doesNotExist('modal should not render');
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`),
|
||||
'Flash message shows that config was saved even if issuer was not.'
|
||||
);
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module('Edit view', function () {
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
test('it defaults to Azure accessType if Azure account fields are already set', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure');
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('azure')).isChecked('Azure accessType is checked');
|
||||
assert.dom(SES.wif.accessType('azure')).isDisabled('Azure accessType is disabled');
|
||||
assert.dom(SES.wif.accessType('wif')).isNotChecked('WIF accessType is not checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSubtext)
|
||||
.hasText('You cannot edit Access Type if you have already saved access credentials.');
|
||||
});
|
||||
|
||||
test('it defaults to WIF accessType if WIF fields are already set', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure-wif');
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert.dom(SES.wif.accessType('azure')).isNotChecked('azure accessType is not checked');
|
||||
assert.dom(SES.wif.accessType('azure')).isDisabled('azure accessType is disabled');
|
||||
assert.dom(GENERAL.inputByAttr('identityTokenAudience')).hasValue(this.config.identityTokenAudience);
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSubtext)
|
||||
.hasText('You cannot edit Access Type if you have already saved access credentials.');
|
||||
assert.dom(GENERAL.ttl.input('Identity token TTL')).hasValue('2'); // 7200 on payload is 2hrs in ttl picker
|
||||
});
|
||||
|
||||
test('it renders issuer if global issuer is already set', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure-wif');
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.issuerConfig.issuer = 'https://foo-bar-blah.com';
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(
|
||||
this.issuerConfig.issuer,
|
||||
`it has the global issuer value of ${this.issuerConfig.issuer}`
|
||||
);
|
||||
});
|
||||
|
||||
test('it allows you to change accessType if record does not have wif or azure values already set', async function (assert) {
|
||||
// the model does not have to be new for a user to see the option to change the access type.
|
||||
// the access type is only disabled if the model has values already set for access type fields.
|
||||
this.config = createConfig(this.store, this.id, 'azure-generic');
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled');
|
||||
assert.dom(SES.wif.accessType('azure')).isNotDisabled('Azure accessType is NOT disabled');
|
||||
});
|
||||
|
||||
test('it shows previously saved config information', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure-generic');
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inputByAttr('subscriptionId')).hasValue(this.config.subscriptionId);
|
||||
assert.dom(GENERAL.inputByAttr('clientId')).hasValue(this.config.clientId);
|
||||
assert.dom(GENERAL.inputByAttr('tenantId')).hasValue(this.config.tenantId);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('clientSecret'))
|
||||
.hasValue('**********', 'clientSecret is masked on edit the value');
|
||||
});
|
||||
|
||||
test('it requires a double click to change the client secret', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure');
|
||||
await this.renderComponent();
|
||||
|
||||
this.server.post(configUrl('azure', this.id), (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(
|
||||
payload.client_secret,
|
||||
'new-secret',
|
||||
'post request was made to azure/config with the updated client_secret.'
|
||||
);
|
||||
});
|
||||
|
||||
await click(GENERAL.enableField('clientSecret'));
|
||||
await click('[data-test-button="toggle-masked"]');
|
||||
await fillIn(GENERAL.inputByAttr('clientSecret'), 'new-secret');
|
||||
await click(GENERAL.saveButton);
|
||||
});
|
||||
});
|
||||
module('isCommunity', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'community';
|
||||
});
|
||||
|
||||
test('it does not show access types but defaults to Azure account fields', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure-generic');
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessTypeSection).doesNotExist('Access type section does not render');
|
||||
assert.dom(GENERAL.inputByAttr('clientId')).hasValue(this.config.clientId);
|
||||
assert.dom(GENERAL.inputByAttr('subscriptionId')).hasValue(this.config.subscriptionId);
|
||||
assert.dom(GENERAL.inputByAttr('tenantId')).hasValue(this.config.tenantId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,866 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import sinon from 'sinon';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import { render, click, fillIn } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
import {
|
||||
expectedConfigKeys,
|
||||
createConfig,
|
||||
configUrl,
|
||||
fillInAzureConfig,
|
||||
fillInAwsConfig,
|
||||
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
import { capabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import { WIF_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines';
|
||||
import waitForError from 'vault/tests/helpers/wait-for-error';
|
||||
|
||||
const allEnginesArray = allEngines(); // saving as const so we don't invoke the method multiple times in the for loop
|
||||
|
||||
module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.flashMessages = this.owner.lookup('service:flash-messages');
|
||||
this.flashMessages.registerTypes(['success', 'danger']);
|
||||
this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success');
|
||||
this.flashDangerSpy = sinon.spy(this.flashMessages, 'danger');
|
||||
this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
this.uid = uuidv4();
|
||||
// stub capabilities so that by default user can read and update issuer
|
||||
this.server.post('/sys/capabilities-self', () => capabilitiesStub('identity/oidc/config', ['sudo']));
|
||||
});
|
||||
|
||||
module('Create view', function () {
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
for (const type of WIF_ENGINES) {
|
||||
test(`${type}: it renders default fields`, async function (assert) {
|
||||
this.id = `${type}-${this.uid}`;
|
||||
this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName;
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.mountConfigModel =
|
||||
type === 'aws'
|
||||
? this.store.createRecord('aws/root-config')
|
||||
: this.store.createRecord(`${type}/config`);
|
||||
this.additionalConfigModel = type === 'aws' ? this.store.createRecord('aws/lease-config') : null;
|
||||
this.mountConfigModel.backend = this.id;
|
||||
this.additionalConfigModel ? (this.additionalConfigModel.backend = this.id) : null; // Add backend to the configs because it's not on the testing snapshot (would come from url)
|
||||
this.type = type;
|
||||
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/>
|
||||
`);
|
||||
assert.dom(SES.configureForm).exists(`it lands on the ${type} configuration form`);
|
||||
assert.dom(SES.wif.accessType(type)).isChecked(`defaults to showing ${type} access type checked`);
|
||||
assert.dom(SES.wif.accessType('wif')).isNotChecked('wif access type is not checked');
|
||||
// toggle grouped fields if it exists
|
||||
const toggleGroup = document.querySelector('[data-test-toggle-group]');
|
||||
toggleGroup ? await click(toggleGroup) : null;
|
||||
|
||||
for (const key of expectedConfigKeys(type, true)) {
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr(key))
|
||||
.exists(
|
||||
`${key} shows for ${type} configuration create section when wif is not the access type`
|
||||
);
|
||||
}
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.doesNotExist(`for ${type}, the issuer does not show when wif is not the access type`);
|
||||
});
|
||||
}
|
||||
|
||||
for (const type of WIF_ENGINES) {
|
||||
test(`${type}: it renders wif fields when user selects wif access type`, async function (assert) {
|
||||
this.id = `${type}-${this.uid}`;
|
||||
this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName;
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.mountConfigModel =
|
||||
type === 'aws'
|
||||
? this.store.createRecord('aws/root-config')
|
||||
: this.store.createRecord(`${type}/config`);
|
||||
this.additionalConfigModel = type === 'aws' ? this.store.createRecord('aws/lease-config') : null;
|
||||
this.mountConfigModel.backend = this.id;
|
||||
this.additionalConfigModel ? (this.additionalConfigModel.backend = this.id) : null;
|
||||
this.type = type;
|
||||
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/>
|
||||
`);
|
||||
await click(SES.wif.accessType('wif'));
|
||||
// check for the wif fields only
|
||||
for (const key of expectedConfigKeys(`${type}-wif`, true)) {
|
||||
if (key === 'Identity token TTL') {
|
||||
assert.dom(GENERAL.ttl.toggle(key)).exists(`${key} shows for ${type} wif section.`);
|
||||
} else {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for ${type} wif section.`);
|
||||
}
|
||||
}
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).exists(`issuer shows for ${type} wif section.`);
|
||||
});
|
||||
}
|
||||
/* This module covers code that is the same for all engines. We run them once against one of the engines.*/
|
||||
module('Engine agnostic', function () {
|
||||
test('it transitions without sending a config or issuer payload on cancel', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.id = `azure-${this.uid}`;
|
||||
this.displayName = 'Azure';
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.mountConfigModel = this.store.createRecord('azure/config');
|
||||
this.mountConfigModel.backend = this.id;
|
||||
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
throw new Error(
|
||||
`Request was made to post the config when it should not have been because the user canceled out of the flow.`
|
||||
);
|
||||
});
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
throw new Error(
|
||||
`Request was made to save the issuer when it should not have been because the user canceled out of the flow.`
|
||||
);
|
||||
});
|
||||
await fillInAzureConfig('withWif');
|
||||
await click(GENERAL.cancelButton);
|
||||
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.');
|
||||
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it throws an error if the getter isWifPluginConfigured is not defined on the model', async function (assert) {
|
||||
const promise = waitForError();
|
||||
this.id = `azure-${this.uid}`;
|
||||
this.displayName = 'Azure';
|
||||
// creating a config that exists but will not have the attribute isWifPluginConfigured on it
|
||||
this.mountConfigModel = this.store.createRecord('ssh/ca-config', { backend: this.id });
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
const err = await promise;
|
||||
assert.true(
|
||||
err.message.includes(
|
||||
`'isWifPluginConfigured' is required to be defined on the config model. Must return a boolean.`
|
||||
),
|
||||
'asserts without isWifPluginConfigured'
|
||||
);
|
||||
});
|
||||
|
||||
test('it allows user to submit the config even if API error occurs on issuer config', async function (assert) {
|
||||
this.id = `aws-${this.uid}`;
|
||||
this.displayName = 'AWS';
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.mountConfigModel = this.store.createRecord('aws/root-config');
|
||||
this.additionalConfigModel = this.store.createRecord('aws/lease-config');
|
||||
this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id;
|
||||
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/>
|
||||
`);
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
assert.true(true, 'post request was made to config/root when issuer failed. test should pass.');
|
||||
});
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
await fillInAwsConfig('withWif');
|
||||
await click(GENERAL.saveButton);
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
|
||||
assert.true(
|
||||
this.flashDangerSpy.calledWith('Issuer was not saved: bad request'),
|
||||
'Flash message shows that issuer was not saved'
|
||||
);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`),
|
||||
'Flash message shows that root was saved even if issuer was not'
|
||||
);
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it surfaces the API error if config save fails, and prevents the user from transitioning', async function (assert) {
|
||||
this.id = `aws-${this.uid}`;
|
||||
this.displayName = 'AWS';
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.mountConfigModel = this.store.createRecord('aws/root-config');
|
||||
this.additionalConfigModel = this.store.createRecord('aws/lease-config');
|
||||
this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id;
|
||||
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/>
|
||||
`);
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', this.id), () => {
|
||||
assert.true(
|
||||
true,
|
||||
'post request was made to config/lease when config/root failed. test should pass.'
|
||||
);
|
||||
});
|
||||
// fill in both lease and root endpoints to ensure that both payloads are attempted to be sent
|
||||
await fillInAwsConfig('withAccess');
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(GENERAL.messageError).exists('API error surfaced to user');
|
||||
assert.dom(GENERAL.inlineError).exists('User shown inline error message');
|
||||
});
|
||||
});
|
||||
|
||||
module('Azure specific', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.id = `azure-${this.uid}`;
|
||||
this.displayName = 'Azure';
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.mountConfigModel = this.store.createRecord('azure/config');
|
||||
this.mountConfigModel.backend = this.id;
|
||||
});
|
||||
test('it clears access type inputs after toggling accessType', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
await fillInAzureConfig('azure');
|
||||
await click(SES.wif.accessType('wif'));
|
||||
await fillInAzureConfig('withWif');
|
||||
await click(SES.wif.accessType('azure'));
|
||||
|
||||
assert
|
||||
.dom(GENERAL.toggleInput('Root password TTL'))
|
||||
.isNotChecked('rootPasswordTtl is cleared after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('clientSecret'))
|
||||
.hasValue('', 'clientSecret is cleared after toggling accessType');
|
||||
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue('', 'issuer shows no value after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasAttribute(
|
||||
'placeholder',
|
||||
'https://vault-test.com',
|
||||
'issuer shows no value after toggling accessType'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('identityTokenAudience'))
|
||||
.hasValue('', 'idTokenAudience is cleared after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.toggleInput('Identity token TTL'))
|
||||
.isNotChecked('identityTokenTtl is cleared after toggling accessType');
|
||||
});
|
||||
|
||||
test('it shows the correct access type subtext', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSubtext)
|
||||
.hasText(
|
||||
'Choose the way to configure access to Azure. Access can be configured either using Azure account credentials or with the Plugin Workload Identity Federation (WIF).'
|
||||
);
|
||||
});
|
||||
|
||||
test('it does not show aws specific note', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
|
||||
assert
|
||||
.dom(SES.configureNote('azure'))
|
||||
.doesNotExist('Note specific to AWS does not show for Azure secret engine when configuring.');
|
||||
});
|
||||
});
|
||||
|
||||
module('AWS specific', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.id = `aws-${this.uid}`;
|
||||
this.displayName = 'AWS';
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.mountConfigModel = this.store.createRecord('aws/root-config');
|
||||
this.additionalConfigModel = this.store.createRecord('aws/lease-config');
|
||||
this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id;
|
||||
});
|
||||
|
||||
test('it clears access type inputs after toggling accessType', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
await fillInAwsConfig('aws');
|
||||
await click(SES.wif.accessType('wif'));
|
||||
await fillInAwsConfig('with-wif');
|
||||
await click(SES.wif.accessType('aws'));
|
||||
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('accessKey'))
|
||||
.hasValue('', 'accessKey is cleared after toggling accessType');
|
||||
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue('', 'issuer shows no value after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasAttribute(
|
||||
'placeholder',
|
||||
'https://vault-test.com',
|
||||
'issuer shows no value after toggling accessType'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('identityTokenAudience'))
|
||||
.hasValue('', 'idTokenAudience is cleared after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.toggleInput('Identity token TTL'))
|
||||
.isNotChecked('identityTokenTtl is cleared after toggling accessType');
|
||||
});
|
||||
|
||||
test('it shows the correct access type subtext', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSubtext)
|
||||
.hasText(
|
||||
'Choose the way to configure access to AWS. Access can be configured either using IAM access keys or with the Plugin Workload Identity Federation (WIF).'
|
||||
);
|
||||
});
|
||||
|
||||
test('it shows validation error if default lease is entered but max lease is not', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/>
|
||||
`);
|
||||
this.server.post(configUrl('aws-lease', this.id), () => {
|
||||
throw new Error(
|
||||
`Request was made to post the config/lease when it should not have been because no data was changed.`
|
||||
);
|
||||
});
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
throw new Error(
|
||||
`Request was made to post the config/root when it should not have been because no data was changed.`
|
||||
);
|
||||
});
|
||||
await click(GENERAL.ttl.toggle('Default Lease TTL'));
|
||||
await fillIn(GENERAL.ttl.input('Default Lease TTL'), '33');
|
||||
await click(GENERAL.saveButton);
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText('Lease TTL and Max Lease TTL are both required if one of them is set.');
|
||||
assert.dom(SES.configureForm).exists('remains on the configuration form');
|
||||
});
|
||||
|
||||
test('it allows user to submit root config even if API error occurs on config/lease config', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/>
|
||||
`);
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
assert.true(
|
||||
true,
|
||||
'post request was made to config/root when config/lease failed. test should pass.'
|
||||
);
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', this.id), () => {
|
||||
return overrideResponse(400, { errors: ['bad request!!'] });
|
||||
});
|
||||
// fill in both lease and root endpoints to ensure that both payloads are attempted to be sent
|
||||
await fillInAwsConfig('withAccess');
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.true(
|
||||
this.flashDangerSpy.calledWith('Lease configuration was not saved: bad request!!'),
|
||||
'Flash message shows that lease was not saved.'
|
||||
);
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it transitions without sending a lease, root, or issuer payload on cancel', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/>
|
||||
`);
|
||||
this.server.post(configUrl('aws', this.id), () => {
|
||||
throw new Error(
|
||||
`Request was made to post the config/root when it should not have been because the user canceled out of the flow.`
|
||||
);
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', this.id), () => {
|
||||
throw new Error(
|
||||
`Request was made to post the config/lease when it should not have been because the user canceled out of the flow.`
|
||||
);
|
||||
});
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
throw new Error(
|
||||
`Request was made to post the identity/oidc/config when it should not have been because the user canceled out of the flow.`
|
||||
);
|
||||
});
|
||||
// fill in both lease and root endpoints to ensure that both payloads are attempted to be sent
|
||||
await fillInAwsConfig('withWif');
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.cancelButton);
|
||||
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.');
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it does show aws specific note', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/>
|
||||
`);
|
||||
|
||||
assert.dom(SES.configureNote('aws')).exists('Note specific to AWS does show when configuring.');
|
||||
});
|
||||
});
|
||||
|
||||
module('Issuer field tests', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.id = `azure-${this.uid}`;
|
||||
this.displayName = 'Azure';
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.issuerConfig.queryIssuerError = true;
|
||||
this.mountConfigModel = this.store.createRecord('azure/config');
|
||||
this.mountConfigModel.backend = this.id;
|
||||
});
|
||||
test('if issuer API error and user changes issuer value, shows specific warning message', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
await click(SES.wif.accessType('wif'));
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://change.me.no.read');
|
||||
await click(GENERAL.saveButton);
|
||||
assert
|
||||
.dom(SES.wif.issuerWarningMessage)
|
||||
.hasText(
|
||||
`You are updating the global issuer config. This will overwrite Vault's current issuer if it exists and may affect other configurations using this value. Continue?`,
|
||||
'modal shows message about overwriting value if it exists'
|
||||
);
|
||||
});
|
||||
|
||||
test('it shows placeholder issuer, and does not call APIs on canceling out of issuer modal', async function (assert) {
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
throw new Error(
|
||||
'Request was made to post the identity/oidc/config when it should not have been because user canceled out of the modal.'
|
||||
);
|
||||
});
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
throw new Error(
|
||||
`Request was made to post the config when it should not have been because the user canceled out of the flow.`
|
||||
);
|
||||
});
|
||||
this.issuerConfig.queryIssuerError = false;
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasAttribute('placeholder', 'https://vault-test.com', 'shows issuer placeholder');
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'shows issuer is empty when not passed');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://bar.foo');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningMessage).exists('issuer modal exists');
|
||||
assert
|
||||
.dom(SES.wif.issuerWarningMessage)
|
||||
.hasText(
|
||||
`You are updating the global issuer config. This will overwrite Vault's current issuer and may affect other configurations using this value. Continue?`,
|
||||
'modal shows message about overwriting value without the noRead: "if it exists" adage'
|
||||
);
|
||||
await click(SES.wif.issuerWarningCancel);
|
||||
assert.dom(SES.wif.issuerWarningMessage).doesNotExist('issuer modal is removed on cancel');
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.');
|
||||
assert.true(this.transitionStub.notCalled, 'Does not redirect');
|
||||
});
|
||||
|
||||
test('it shows modal when updating issuer and calls correct APIs on save', async function (assert) {
|
||||
const newIssuer = `http://bar.${uuidv4()}`;
|
||||
this.server.post('/identity/oidc/config', (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(payload, { issuer: newIssuer }, 'payload for issuer is correct');
|
||||
return {
|
||||
id: 'identity-oidc-config', // id needs to match the id on secret-engine-helpers createIssuerConfig
|
||||
data: null,
|
||||
warnings: [
|
||||
'If "issuer" is set explicitly, all tokens must be validated against that address, including those issued by secondary clusters. Setting issuer to "" will restore the default behavior of using the cluster\'s api_addr as the issuer.',
|
||||
],
|
||||
};
|
||||
});
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
throw new Error(
|
||||
`Request was made to post the config when it should not have been because no data was changed.`
|
||||
);
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'issuer defaults to empty string');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), newIssuer);
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningMessage).exists('issuer warning modal exists');
|
||||
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith('Issuer saved successfully'),
|
||||
'Success flash message called for issuer'
|
||||
);
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows modal when modifying the issuer, has correct payload, and shows flash message on fail', async function (assert) {
|
||||
assert.expect(7);
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
assert.true(
|
||||
true,
|
||||
'post request was made to azure config when unsetting the issuer. test should pass.'
|
||||
);
|
||||
});
|
||||
this.server.post('/identity/oidc/config', (_, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(payload, { issuer: 'http://foo.bar' }, 'correctly sets the issuer');
|
||||
return overrideResponse(403);
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('');
|
||||
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://foo.bar');
|
||||
await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'some-value');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningMessage).exists('issuer warning modal exists');
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
|
||||
assert.true(
|
||||
this.flashDangerSpy.calledWith('Issuer was not saved: permission denied'),
|
||||
'shows danger flash for issuer save'
|
||||
);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`),
|
||||
"calls the config flash message not the issuer's"
|
||||
);
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it does not clear global issuer when toggling accessType', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(this.issuerConfig.issuer, 'issuer is what is sent in by the model on first load');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://ive-changed');
|
||||
await click(SES.wif.accessType('azure'));
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(
|
||||
this.issuerConfig.issuer,
|
||||
'issuer value is still the same global value after toggling accessType'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module('isCommunity', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'community';
|
||||
});
|
||||
|
||||
for (const type of WIF_ENGINES) {
|
||||
test(`${type}: it renders fields`, async function (assert) {
|
||||
this.id = `${type}-${this.uid}`;
|
||||
this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName;
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.mountConfigModel =
|
||||
type === 'aws'
|
||||
? this.store.createRecord('aws/root-config')
|
||||
: type === 'ssh'
|
||||
? this.store.createRecord('ssh/ca-config')
|
||||
: this.store.createRecord(`${type}/config`);
|
||||
this.additionalConfigModel = type === 'aws' ? this.store.createRecord('aws/lease-config') : null;
|
||||
this.mountConfigModel.backend = this.id;
|
||||
this.additionalConfigModel ? (this.additionalConfigModel.backend = this.id) : null;
|
||||
this.type = type;
|
||||
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/>
|
||||
`);
|
||||
assert.dom(SES.configureForm).exists(`lands on the ${type} configuration form`);
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSection)
|
||||
.doesNotExist('Access type section does not render for a community user');
|
||||
// toggle grouped fields if it exists
|
||||
const toggleGroup = document.querySelector('[data-test-toggle-group]');
|
||||
toggleGroup ? await click(toggleGroup) : null;
|
||||
// check all the form fields are present
|
||||
for (const key of expectedConfigKeys(type, true)) {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for ${type} account access section.`);
|
||||
}
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module('Edit view', function () {
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
for (const type of WIF_ENGINES) {
|
||||
test(`${type}: it defaults to WIF accessType if WIF fields are already set`, async function (assert) {
|
||||
this.id = `${type}-${this.uid}`;
|
||||
this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName;
|
||||
this.mountConfigModel = createConfig(this.store, this.id, `${type}-wif`);
|
||||
this.type = type;
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} />
|
||||
`);
|
||||
assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert.dom(SES.wif.accessType(type)).isNotChecked(`${type} accessType is not checked`);
|
||||
assert.dom(SES.wif.accessType(type)).isDisabled(`${type} accessType is disabled`);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('identityTokenAudience'))
|
||||
.hasValue(this.mountConfigModel.identityTokenAudience);
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSubtext)
|
||||
.hasText('You cannot edit Access Type if you have already saved access credentials.');
|
||||
assert.dom(GENERAL.ttl.input('Identity token TTL')).hasValue('2'); // 7200 on payload is 2hrs in ttl picker
|
||||
});
|
||||
}
|
||||
|
||||
for (const type of WIF_ENGINES) {
|
||||
test(`${type}: it renders issuer if global issuer is already set`, async function (assert) {
|
||||
this.id = `${type}-${this.uid}`;
|
||||
this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName;
|
||||
this.mountConfigModel = createConfig(this.store, this.id, `${type}-wif`);
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.issuerConfig.issuer = 'https://foo-bar-blah.com';
|
||||
this.type = type;
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
|
||||
assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(
|
||||
this.issuerConfig.issuer,
|
||||
`it has the global issuer value of ${this.issuerConfig.issuer}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module('Azure specific', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.id = `azure-${this.uid}`;
|
||||
this.mountConfigModel = createConfig(this.store, this.id, 'azure');
|
||||
});
|
||||
|
||||
test('it defaults to Azure accessType if Azure account fields are already set', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='Azure' @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
|
||||
assert.dom(SES.wif.accessType('azure')).isChecked('Azure accessType is checked');
|
||||
assert.dom(SES.wif.accessType('azure')).isDisabled('Azure accessType is disabled');
|
||||
assert.dom(SES.wif.accessType('wif')).isNotChecked('WIF accessType is not checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSubtext)
|
||||
.hasText('You cannot edit Access Type if you have already saved access credentials.');
|
||||
});
|
||||
|
||||
test('it allows you to change accessType if record does not have wif or azure values already set', async function (assert) {
|
||||
this.mountConfigModel = createConfig(this.store, this.id, 'azure-generic');
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='Azure' @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
|
||||
assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled');
|
||||
assert.dom(SES.wif.accessType('azure')).isNotDisabled('Azure accessType is NOT disabled');
|
||||
});
|
||||
|
||||
test('it shows previously saved config information', async function (assert) {
|
||||
this.id = `azure-${this.uid}`;
|
||||
this.mountConfigModel = createConfig(this.store, this.id, 'azure-generic');
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='Azure' @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
assert.dom(GENERAL.inputByAttr('subscriptionId')).hasValue(this.mountConfigModel.subscriptionId);
|
||||
assert.dom(GENERAL.inputByAttr('clientId')).hasValue(this.mountConfigModel.clientId);
|
||||
assert.dom(GENERAL.inputByAttr('tenantId')).hasValue(this.mountConfigModel.tenantId);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('clientSecret'))
|
||||
.hasValue('**********', 'clientSecret is masked on edit the value');
|
||||
});
|
||||
|
||||
test('it requires a double click to change the client secret', async function (assert) {
|
||||
this.id = `azure-${this.uid}`;
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='Azure' @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
|
||||
this.server.post(configUrl('azure', this.id), (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(
|
||||
payload.client_secret,
|
||||
'new-secret',
|
||||
'post request was made to azure/config with the updated client_secret.'
|
||||
);
|
||||
});
|
||||
|
||||
await click(GENERAL.enableField('clientSecret'));
|
||||
await click('[data-test-button="toggle-masked"]');
|
||||
await fillIn(GENERAL.inputByAttr('clientSecret'), 'new-secret');
|
||||
await click(GENERAL.saveButton);
|
||||
});
|
||||
});
|
||||
|
||||
module('AWS specific', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.id = `aws-${this.uid}`;
|
||||
this.mountConfigModel = createConfig(this.store, this.id, 'aws');
|
||||
});
|
||||
test('it defaults to IAM accessType if IAM fields are already set', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='AWS' @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
assert.dom(SES.wif.accessType('aws')).isChecked('IAM accessType is checked');
|
||||
assert.dom(SES.wif.accessType('aws')).isDisabled('IAM accessType is disabled');
|
||||
assert.dom(SES.wif.accessType('wif')).isNotChecked('WIF accessType is not checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSubtext)
|
||||
.hasText('You cannot edit Access Type if you have already saved access credentials.');
|
||||
});
|
||||
|
||||
test('it allows you to change access type if record does not have wif or iam values already set', async function (assert) {
|
||||
this.mountConfigModel = createConfig(this.store, this.id, 'aws-no-access');
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='AWS' @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
|
||||
`);
|
||||
assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled');
|
||||
assert.dom(SES.wif.accessType('aws')).isNotDisabled('IAM accessType is NOT disabled');
|
||||
});
|
||||
|
||||
test('it shows previously saved root and lease information', async function (assert) {
|
||||
this.additionalConfigModel = createConfig(this.store, this.id, 'aws-lease');
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='AWS' @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/>
|
||||
`);
|
||||
|
||||
assert.dom(GENERAL.inputByAttr('accessKey')).hasValue(this.mountConfigModel.accessKey);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('secretKey'))
|
||||
.hasValue('**********', 'secretKey is masked on edit the value');
|
||||
|
||||
await click(GENERAL.toggleGroup('Root config options'));
|
||||
assert.dom(GENERAL.inputByAttr('region')).hasValue(this.mountConfigModel.region);
|
||||
assert.dom(GENERAL.inputByAttr('iamEndpoint')).hasValue(this.mountConfigModel.iamEndpoint);
|
||||
assert.dom(GENERAL.inputByAttr('stsEndpoint')).hasValue(this.mountConfigModel.stsEndpoint);
|
||||
assert.dom(GENERAL.inputByAttr('maxRetries')).hasValue('1');
|
||||
// Check lease config values
|
||||
assert.dom(GENERAL.ttl.input('Default Lease TTL')).hasValue('50');
|
||||
assert.dom(GENERAL.ttl.input('Max Lease TTL')).hasValue('55');
|
||||
});
|
||||
|
||||
test('it requires a double click to change the secret key', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='AWS' @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} />
|
||||
`);
|
||||
|
||||
this.server.post(configUrl('aws', this.id), (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(
|
||||
payload.secret_key,
|
||||
'new-secret',
|
||||
'post request was made to config/root with the updated secret_key.'
|
||||
);
|
||||
});
|
||||
|
||||
await click(GENERAL.enableField('secretKey'));
|
||||
await click('[data-test-button="toggle-masked"]');
|
||||
await fillIn(GENERAL.inputByAttr('secretKey'), 'new-secret');
|
||||
await click(GENERAL.saveButton);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module('isCommunity', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'community';
|
||||
});
|
||||
for (const type of WIF_ENGINES) {
|
||||
test(`${type}:it does not show access type but defaults to type "account" fields`, async function (assert) {
|
||||
this.id = `${type}-${this.uid}`;
|
||||
this.mountConfigModel = createConfig(this.store, this.id, `${type}-generic`);
|
||||
this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName;
|
||||
this.type = type;
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/>
|
||||
`);
|
||||
assert.dom(SES.wif.accessTypeSection).doesNotExist('Access type section does not render');
|
||||
// toggle grouped fields if it exists
|
||||
const toggleGroup = document.querySelector('[data-test-toggle-group]');
|
||||
toggleGroup ? await click(toggleGroup) : null;
|
||||
|
||||
for (const key of expectedConfigKeys(type, true)) {
|
||||
if (key === 'secretKey' || key === 'clientSecret') return; // these keys are not returned by the API
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr(key))
|
||||
.hasValue(
|
||||
this.mountConfigModel[key],
|
||||
`${key} for ${type}: has the expected value set on the config`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
22
ui/types/vault/models/aws/lease-config.d.ts
vendored
22
ui/types/vault/models/aws/lease-config.d.ts
vendored
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Model from '@ember-data/model';
|
||||
import type { ModelValidations } from 'vault/vault/app-types';
|
||||
|
||||
export default class AwsLeaseConfig extends Model {
|
||||
backend: any;
|
||||
leaseMax: any;
|
||||
lease: any;
|
||||
get attrs(): string[];
|
||||
// for some reason the following Model attrs don't exist on the Model definition
|
||||
changedAttributes(): {
|
||||
[key: string]: unknown[];
|
||||
};
|
||||
isNew: boolean;
|
||||
save(): void;
|
||||
unloadRecord(): void;
|
||||
validate(): ModelValidations;
|
||||
}
|
||||
32
ui/types/vault/models/aws/root-config.d.ts
vendored
32
ui/types/vault/models/aws/root-config.d.ts
vendored
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import type Model from '@ember-data/model';
|
||||
|
||||
export default class AwsRootConfig extends Model {
|
||||
backend: any;
|
||||
accessKey: any;
|
||||
secretKey: any;
|
||||
roleArn: any;
|
||||
identityTokenAudience: any;
|
||||
identityTokenTtl: any;
|
||||
region: any;
|
||||
iamEndpoint: any;
|
||||
stsEndpoint: any;
|
||||
maxRetries: any;
|
||||
get attrs(): any;
|
||||
get fieldGroupsWif(): any;
|
||||
get fieldGroupsIam(): any;
|
||||
formFieldGroups(accessType?: string): {
|
||||
[key: string]: string[];
|
||||
}[];
|
||||
// for some reason the following Model attrs don't exist on the Model definition
|
||||
changedAttributes(): {
|
||||
[key: string]: unknown[];
|
||||
};
|
||||
isNew: boolean;
|
||||
save(): void;
|
||||
unloadRecord(): void;
|
||||
}
|
||||
34
ui/types/vault/models/azure/config.d.ts
vendored
34
ui/types/vault/models/azure/config.d.ts
vendored
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import type Model from '@ember-data/model';
|
||||
import type { ModelValidations } from 'vault/app-types';
|
||||
|
||||
export default class AzureConfig extends Model {
|
||||
backend: string;
|
||||
subscriptionId: string | undefined;
|
||||
tenantId: string | undefined;
|
||||
clientId: string | undefined;
|
||||
clientSecret: string | undefined;
|
||||
identityTokenAudience: string | undefined;
|
||||
identityTokenTtl: any;
|
||||
environment: string | undefined;
|
||||
rootPasswordTtl: string | undefined;
|
||||
|
||||
get displayAttrs(): any;
|
||||
get isWifPluginConfigured(): boolean;
|
||||
get isAzureAccountConfigured(): boolean;
|
||||
get fieldGroupsWif(): any;
|
||||
get fieldGroupsAzure(): any;
|
||||
formFieldGroups(accessType?: string): {
|
||||
[key: string]: string[];
|
||||
}[];
|
||||
changedAttributes(): {
|
||||
[key: string]: unknown[];
|
||||
};
|
||||
isNew: boolean;
|
||||
save(): void;
|
||||
unloadRecord(): void;
|
||||
}
|
||||
28
ui/types/vault/models/secret-engine/additional-config.d.ts
vendored
Normal file
28
ui/types/vault/models/secret-engine/additional-config.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import type Model from '@ember-data/model';
|
||||
import type { ModelValidations, FormField } from 'vault/app-types';
|
||||
|
||||
export default class SecretEngineAdditionalConfigModel extends Model {
|
||||
backend: string;
|
||||
type: string;
|
||||
// aws lease
|
||||
leaseMax: any;
|
||||
lease: any;
|
||||
|
||||
get displayAttrs(): any;
|
||||
|
||||
formFields: FormField[];
|
||||
changedAttributes(): {
|
||||
[key: string]: unknown[];
|
||||
};
|
||||
isNew: boolean;
|
||||
save(): void;
|
||||
unloadRecord(): void;
|
||||
destroyRecord(): void;
|
||||
rollbackAttributes(): void;
|
||||
validate(): ModelValidations;
|
||||
}
|
||||
53
ui/types/vault/models/secret-engine/mount-config.d.ts
vendored
Normal file
53
ui/types/vault/models/secret-engine/mount-config.d.ts
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import type Model from '@ember-data/model';
|
||||
import type { ModelValidations, FormFieldGroups } from 'vault/app-types';
|
||||
|
||||
export default class SecretEngineMountConfigModel extends Model {
|
||||
backend: string;
|
||||
type: string;
|
||||
// aws
|
||||
accessKey: any;
|
||||
secretKey: any;
|
||||
roleArn: any;
|
||||
region: any;
|
||||
iamEndpoint: any;
|
||||
stsEndpoint: any;
|
||||
maxRetries: any;
|
||||
// azure
|
||||
subscriptionId: string | undefined;
|
||||
tenantId: string | undefined;
|
||||
clientId: string | undefined;
|
||||
clientSecret: string | undefined;
|
||||
environment: string | undefined;
|
||||
rootPasswordTtl: string | undefined;
|
||||
// gcp
|
||||
credentials: string | undefined;
|
||||
ttl: any;
|
||||
maxTtl: any;
|
||||
secretAccountEmail: string | undefined;
|
||||
// wif
|
||||
identityTokenAudience: string | undefined;
|
||||
identityTokenTtl: any;
|
||||
|
||||
get displayAttrs(): any;
|
||||
get isConfigured(): boolean; // used only for secret engines that return a 200 when configuration has not be set.
|
||||
get isWifPluginConfigured(): boolean;
|
||||
get isAccountPluginConfigured(): boolean;
|
||||
get fieldGroupsWif(): any;
|
||||
get fieldGroupsAccount(): any;
|
||||
|
||||
formFieldGroups: FormFieldGroups[];
|
||||
|
||||
changedAttributes(): {
|
||||
[key: string]: unknown[];
|
||||
};
|
||||
isNew: boolean;
|
||||
save(): void;
|
||||
unloadRecord(): void;
|
||||
destroyRecord(): void;
|
||||
rollbackAttributes(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user