mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +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
|
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" />
|
<NamespaceReminder @mode="save" @noun="configuration" />
|
||||||
<MessageError @errorMessage={{this.errorMessage}} />
|
<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">
|
<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}}
|
{{#if this.version.isEnterprise}}
|
||||||
<fieldset class="field form-fieldset" id="protection" data-test-access-type-section>
|
<fieldset class="field form-fieldset" id="protection" data-test-access-type-section>
|
||||||
<legend class="is-label">Access Type</legend>
|
<legend class="is-label">Access Type</legend>
|
||||||
@@ -15,22 +25,24 @@
|
|||||||
{{#if this.disableAccessType}}
|
{{#if this.disableAccessType}}
|
||||||
You cannot edit Access Type if you have already saved access credentials.
|
You cannot edit Access Type if you have already saved access credentials.
|
||||||
{{else}}
|
{{else}}
|
||||||
Choose the way to configure access to Azure. Access can be configured either using an Azure account or with the
|
Choose the way to configure access to
|
||||||
Plugin Workload Identity Federation (WIF).
|
{{@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}}
|
{{/if}}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<RadioButton
|
<RadioButton
|
||||||
id="access-type-azure"
|
id="access-type-{{@type}}"
|
||||||
name="azure"
|
name="account"
|
||||||
class="radio"
|
class="radio"
|
||||||
data-test-access-type="azure"
|
data-test-access-type={{@type}}
|
||||||
@value="azure"
|
@value="account"
|
||||||
@groupValue={{this.accessType}}
|
@groupValue={{this.accessType}}
|
||||||
@onChange={{fn this.onChangeAccessType "azure"}}
|
@onChange={{this.onChangeAccessType}}
|
||||||
@disabled={{this.disableAccessType}}
|
@disabled={{this.disableAccessType}}
|
||||||
/>
|
/>
|
||||||
<label for="access-type-azure">Azure account credentials</label>
|
<label for="access-type-{{@type}}">{{@displayName}} account credentials</label>
|
||||||
<RadioButton
|
<RadioButton
|
||||||
id="access-type-wif"
|
id="access-type-wif"
|
||||||
name="wif"
|
name="wif"
|
||||||
@@ -38,7 +50,7 @@
|
|||||||
data-test-access-type="wif"
|
data-test-access-type="wif"
|
||||||
@value="wif"
|
@value="wif"
|
||||||
@groupValue={{this.accessType}}
|
@groupValue={{this.accessType}}
|
||||||
@onChange={{fn this.onChangeAccessType "wif"}}
|
@onChange={{this.onChangeAccessType}}
|
||||||
@disabled={{this.disableAccessType}}
|
@disabled={{this.disableAccessType}}
|
||||||
/>
|
/>
|
||||||
<label for="access-type-wif">Workload Identity Federation</label>
|
<label for="access-type-wif">Workload Identity Federation</label>
|
||||||
@@ -46,21 +58,38 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if (eq this.accessType "wif")}}
|
{{#if (eq this.accessType "wif")}}
|
||||||
{{! WIF Fields }}
|
{{! if access type is "wif" display Issuer and WIF fields }}
|
||||||
{{#each @issuerConfig.displayAttrs as |attr|}}
|
{{#each @issuerConfig.displayAttrs as |attr|}}
|
||||||
<FormField @attr={{attr}} @model={{@issuerConfig}} />
|
<FormField @attr={{attr}} @model={{@issuerConfig}} />
|
||||||
{{/each}}
|
{{/each}}
|
||||||
<FormFieldGroups @model={{@model}} @mode={{if @model.isNew "create" "edit"}} @groupName="fieldGroupsWif" />
|
|
||||||
{{else}}
|
|
||||||
{{! Azure Account Fields }}
|
|
||||||
<FormFieldGroups
|
<FormFieldGroups
|
||||||
@model={{@model}}
|
@model={{@mountConfigModel}}
|
||||||
@mode={{if @model.isNew "create" "edit"}}
|
@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}}
|
@useEnableInput={{true}}
|
||||||
@groupName="fieldGroupsAzure"
|
@groupName="fieldGroupsAccount"
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/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>
|
</div>
|
||||||
|
|
||||||
<Hds::ButtonSet>
|
<Hds::ButtonSet>
|
||||||
<Hds::Button
|
<Hds::Button
|
||||||
@text="Save"
|
@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
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isPresent } from '@ember/utils';
|
|
||||||
import { service } from '@ember/service';
|
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
|
import { WIF_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines';
|
||||||
|
|
||||||
const CONFIG_ATTRS = {
|
export default class SecretsBackendConfigurationEditController extends Controller {
|
||||||
// ssh
|
get isWifEngine() {
|
||||||
configured: false,
|
return WIF_ENGINES.includes(this.model.type);
|
||||||
|
}
|
||||||
// aws root config
|
get displayName() {
|
||||||
iamEndpoint: null,
|
return allEngines().find((engine) => engine.type === this.model.type)?.displayName;
|
||||||
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);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -74,21 +74,21 @@ export default class AwsRootConfig extends Model {
|
|||||||
return formFields.filter((attr) => attr.name !== 'secretKey');
|
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() {
|
get fieldGroupsWif() {
|
||||||
return fieldToAttrs(this, this.formFieldGroups('wif'));
|
return fieldToAttrs(this, this.formFieldGroups('wif'));
|
||||||
}
|
}
|
||||||
|
|
||||||
get fieldGroupsIam() {
|
get fieldGroupsAccount() {
|
||||||
return fieldToAttrs(this, this.formFieldGroups('iam'));
|
return fieldToAttrs(this, this.formFieldGroups('account'));
|
||||||
}
|
}
|
||||||
|
|
||||||
formFieldGroups(accessType = 'iam') {
|
formFieldGroups(accessType = 'account') {
|
||||||
const formFieldGroups = [];
|
const formFieldGroups = [];
|
||||||
if (accessType === 'wif') {
|
if (accessType === 'wif') {
|
||||||
formFieldGroups.push({ default: ['roleArn', 'identityTokenAudience', 'identityTokenTtl'] });
|
formFieldGroups.push({ default: ['roleArn', 'identityTokenAudience', 'identityTokenTtl'] });
|
||||||
}
|
}
|
||||||
if (accessType === 'iam') {
|
if (accessType === 'account') {
|
||||||
formFieldGroups.push({ default: ['accessKey', 'secretKey'] });
|
formFieldGroups.push({ default: ['accessKey', 'secretKey'] });
|
||||||
}
|
}
|
||||||
formFieldGroups.push({
|
formFieldGroups.push({
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default class AzureConfig extends Model {
|
|||||||
return !!this.identityTokenAudience || !!this.identityTokenTtl;
|
return !!this.identityTokenAudience || !!this.identityTokenTtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isAzureAccountConfigured() {
|
get isAccountPluginConfigured() {
|
||||||
// clientSecret is not checked here because it's never return by the API
|
// clientSecret is not checked here because it's never return by the API
|
||||||
// however it is an Azure account field
|
// however it is an Azure account field
|
||||||
return !!this.rootPasswordTtl;
|
return !!this.rootPasswordTtl;
|
||||||
@@ -79,16 +79,16 @@ export default class AzureConfig extends Model {
|
|||||||
return formFields.filter((attr) => attr.name !== 'clientSecret');
|
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() {
|
get fieldGroupsWif() {
|
||||||
return fieldToAttrs(this, this.formFieldGroups('wif'));
|
return fieldToAttrs(this, this.formFieldGroups('wif'));
|
||||||
}
|
}
|
||||||
|
|
||||||
get fieldGroupsAzure() {
|
get fieldGroupsAccount() {
|
||||||
return fieldToAttrs(this, this.formFieldGroups('azure'));
|
return fieldToAttrs(this, this.formFieldGroups('account'));
|
||||||
}
|
}
|
||||||
|
|
||||||
formFieldGroups(accessType = 'azure') {
|
formFieldGroups(accessType = 'account') {
|
||||||
const formFieldGroups = [];
|
const formFieldGroups = [];
|
||||||
formFieldGroups.push({
|
formFieldGroups.push({
|
||||||
default: ['subscriptionId', 'tenantId', 'clientId', 'environment'],
|
default: ['subscriptionId', 'tenantId', 'clientId', 'environment'],
|
||||||
@@ -98,7 +98,7 @@ export default class AzureConfig extends Model {
|
|||||||
default: ['identityTokenAudience', 'identityTokenTtl'],
|
default: ['identityTokenAudience', 'identityTokenTtl'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (accessType === 'azure') {
|
if (accessType === 'account') {
|
||||||
formFieldGroups.push({
|
formFieldGroups.push({
|
||||||
default: ['clientSecret', 'rootPasswordTtl'],
|
default: ['clientSecret', 'rootPasswordTtl'],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import type VersionService from 'vault/services/version';
|
|||||||
// It generates config models based on the engine type.
|
// It generates config models based on the engine type.
|
||||||
// Saving and updating of those models are done within the engine specific components.
|
// Saving and updating of those models are done within the engine specific components.
|
||||||
|
|
||||||
const CONFIG_ADAPTERS_PATHS: Record<string, string[]> = {
|
const MOUNT_CONFIG_MODEL_NAMES: Record<string, string[]> = {
|
||||||
aws: ['aws/lease-config', 'aws/root-config'],
|
aws: ['aws/root-config', 'aws/lease-config'],
|
||||||
azure: ['azure/config'],
|
azure: ['azure/config'],
|
||||||
ssh: ['ssh/ca-config'],
|
ssh: ['ssh/ca-config'],
|
||||||
};
|
};
|
||||||
@@ -29,6 +29,19 @@ export default class SecretsBackendConfigurationEdit extends Route {
|
|||||||
@service declare readonly store: Store;
|
@service declare readonly store: Store;
|
||||||
@service declare readonly version: VersionService;
|
@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() {
|
async model() {
|
||||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||||
const secretEngineRecord = this.modelFor('vault.cluster.secrets.backend') as SecretEngineModel;
|
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.
|
// generate the model based on the engine type.
|
||||||
// and pre-set model with type and backend e.g. {type: ssh, id: ssh-123}
|
// and pre-set model with type and backend e.g. {type: ssh, id: ssh-123}
|
||||||
const model: Record<string, unknown> = { type, id: backend };
|
const model: Record<string, unknown> = { type, id: backend };
|
||||||
for (const adapterPath of CONFIG_ADAPTERS_PATHS[type] as string[]) {
|
for (const modelName of MOUNT_CONFIG_MODEL_NAMES[type] as string[]) {
|
||||||
// convert the adapterPath with a name that can be passed to the components
|
// create a key that corresponds with the model order
|
||||||
// ex: adapterPath = ssh/ca-config, convert to: ssh-ca-config so that you can pass to component @model={{this.model.ssh-ca-config}}
|
// 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 = adapterPath.replace(/\//g, '-');
|
const standardizedKey = this.standardizedModelName(type, modelName);
|
||||||
try {
|
try {
|
||||||
const configModel = await this.store.queryRecord(adapterPath, {
|
const configModel = await this.store.queryRecord(modelName, {
|
||||||
backend,
|
backend,
|
||||||
type,
|
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
|
// 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 the engine is not configured we update the record to get the default values
|
||||||
if (!configModel.isConfigured && type === 'azure') {
|
if (!configModel.isConfigured && type === 'azure') {
|
||||||
model[standardizedKey] = await this.store.createRecord(adapterPath, {
|
model[standardizedKey] = await this.store.createRecord(modelName, {
|
||||||
backend,
|
backend,
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
@@ -71,7 +84,7 @@ export default class SecretsBackendConfigurationEdit extends Route {
|
|||||||
e.httpStatus === 404 ||
|
e.httpStatus === 404 ||
|
||||||
(type === 'ssh' && e.httpStatus === 400 && errorMessage(e) === `keys haven't been configured yet`)
|
(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,
|
backend,
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<p.levelLeft>
|
<p.levelLeft>
|
||||||
<h1 class="title is-3" data-test-backend-configure-title={{this.model.type}}>
|
<h1 class="title is-3" data-test-backend-configure-title={{this.model.type}}>
|
||||||
Configure
|
Configure
|
||||||
{{get (options-for-backend this.model.type) "displayName"}}
|
{{this.displayName}}
|
||||||
</h1>
|
</h1>
|
||||||
</p.levelLeft>
|
</p.levelLeft>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
@@ -28,19 +28,16 @@
|
|||||||
</ToolbarActions>
|
</ToolbarActions>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
||||||
{{#if (eq this.model.type "aws")}}
|
{{#if (eq this.model.type "ssh")}}
|
||||||
<SecretEngine::ConfigureAws
|
<SecretEngine::ConfigureSsh @model={{this.model.mount-config-model}} @id={{this.model.id}} />
|
||||||
@leaseConfig={{this.model.aws-lease-config}}
|
{{! This "else if" check is preventive. As of writing, all engines using this route, but "ssh", are wif engines }}
|
||||||
@rootConfig={{this.model.aws-root-config}}
|
{{else if this.isWifEngine}}
|
||||||
@issuerConfig={{this.model.identity-oidc-config}}
|
<SecretEngine::ConfigureWif
|
||||||
@backendPath={{this.model.id}}
|
|
||||||
/>
|
|
||||||
{{else if (eq this.model.type "azure")}}
|
|
||||||
<SecretEngine::ConfigureAzure
|
|
||||||
@model={{this.model.azure-config}}
|
|
||||||
@backendPath={{this.model.id}}
|
@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}}
|
@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}}
|
{{/if}}
|
||||||
@@ -34,10 +34,12 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||||||
this.store = this.owner.lookup('service:store');
|
this.store = this.owner.lookup('service:store');
|
||||||
this.flashSuccessSpy = spy(flash, 'success');
|
this.flashSuccessSpy = spy(flash, 'success');
|
||||||
this.flashInfoSpy = spy(flash, 'info');
|
this.flashInfoSpy = spy(flash, 'info');
|
||||||
|
this.flashDangerSpy = spy(flash, 'danger');
|
||||||
this.version = this.owner.lookup('service:version');
|
this.version = this.owner.lookup('service:version');
|
||||||
this.uid = uuidv4();
|
this.uid = uuidv4();
|
||||||
return authPage.login();
|
return authPage.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
module('isEnterprise', function (hooks) {
|
module('isEnterprise', function (hooks) {
|
||||||
hooks.beforeEach(function () {
|
hooks.beforeEach(function () {
|
||||||
this.version.type = 'enterprise';
|
this.version.type = 'enterprise';
|
||||||
@@ -62,7 +64,13 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||||||
await click(SES.configure);
|
await click(SES.configure);
|
||||||
assert.strictEqual(currentURL(), `/vault/secrets/${path}/configuration/edit`);
|
assert.strictEqual(currentURL(), `/vault/secrets/${path}/configuration/edit`);
|
||||||
assert.dom(SES.configureTitle('aws')).hasText('Configure AWS');
|
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
|
// cleanup
|
||||||
await runCmd(`delete sys/mounts/${path}`);
|
await runCmd(`delete sys/mounts/${path}`);
|
||||||
});
|
});
|
||||||
@@ -83,9 +91,8 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||||||
await enablePage.enable('aws', path);
|
await enablePage.enable('aws', path);
|
||||||
|
|
||||||
this.server.post(configUrl('aws-lease', path), () => {
|
this.server.post(configUrl('aws-lease', path), () => {
|
||||||
assert.false(
|
throw new Error(
|
||||||
true,
|
'A POST request was made to config/lease when it should not because no data was changed.'
|
||||||
'post request was made to config/lease when no data was changed. test should fail.'
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,13 +100,13 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||||||
await click(SES.configure);
|
await click(SES.configure);
|
||||||
await fillInAwsConfig('withWif');
|
await fillInAwsConfig('withWif');
|
||||||
await click(GENERAL.saveButton);
|
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);
|
await click(SES.wif.issuerWarningSave);
|
||||||
// three flash messages, the first is about mounting the engine, only care about the last two
|
// three flash messages, the first is about mounting the engine, only care about the last two
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
this.flashSuccessSpy.args[1][0],
|
this.flashSuccessSpy.args[1][0],
|
||||||
`Successfully saved ${path}'s root configuration.`,
|
`Successfully saved ${path}'s configuration.`,
|
||||||
'first flash message about the root config.'
|
'first flash message about the first model config.'
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
this.flashSuccessSpy.args[2][0],
|
this.flashSuccessSpy.args[2][0],
|
||||||
@@ -124,11 +131,11 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||||||
const type = 'aws';
|
const type = 'aws';
|
||||||
this.server.get(`${path}/config/root`, (schema, req) => {
|
this.server.get(`${path}/config/root`, (schema, req) => {
|
||||||
const payload = JSON.parse(req.requestBody);
|
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 } };
|
return { data: { id: path, type, attributes: payload } };
|
||||||
});
|
});
|
||||||
this.server.get(`identity/oidc/config`, () => {
|
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);
|
await enablePage.enable(type, path);
|
||||||
createConfig(this.store, path, type); // create the aws root config in the store
|
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);
|
await enablePage.enable('aws', path);
|
||||||
|
|
||||||
this.server.post(configUrl('aws-lease', path), () => {
|
this.server.post(configUrl('aws-lease', path), () => {
|
||||||
assert.false(
|
throw new Error(`post request was made to config/lease when it should not have been.`);
|
||||||
true,
|
|
||||||
'post request was made to config/lease when no data was changed. test should fail.'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await click(SES.configTab);
|
await click(SES.configTab);
|
||||||
@@ -156,8 +160,8 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||||||
await fillInAwsConfig('withAccess');
|
await fillInAwsConfig('withAccess');
|
||||||
await click(GENERAL.saveButton);
|
await click(GENERAL.saveButton);
|
||||||
assert.true(
|
assert.true(
|
||||||
this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s root configuration.`),
|
this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s configuration.`),
|
||||||
'Success flash message is rendered showing the root configuration was saved.'
|
'Success flash message is rendered showing the configuration was saved.'
|
||||||
);
|
);
|
||||||
assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', 'Access Key has been set.');
|
assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', 'Access Key has been set.');
|
||||||
assert
|
assert
|
||||||
@@ -190,40 +194,12 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||||||
await runCmd(`delete sys/mounts/${path}`);
|
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) {
|
test('it shows AWS mount configuration details', async function (assert) {
|
||||||
const path = `aws-${this.uid}`;
|
const path = `aws-${this.uid}`;
|
||||||
const type = 'aws';
|
const type = 'aws';
|
||||||
this.server.get(`${path}/config/root`, (schema, req) => {
|
this.server.get(`${path}/config/root`, (schema, req) => {
|
||||||
const payload = JSON.parse(req.requestBody);
|
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 } };
|
return { data: { id: path, type, attributes: payload } };
|
||||||
});
|
});
|
||||||
await enablePage.enable(type, path);
|
await enablePage.enable(type, path);
|
||||||
@@ -280,19 +256,6 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||||||
await runCmd(`delete sys/mounts/${path}`);
|
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) {
|
test('it should not make a post request if lease or root data was unchanged', async function (assert) {
|
||||||
assert.expect(3);
|
assert.expect(3);
|
||||||
const path = `aws-${this.uid}`;
|
const path = `aws-${this.uid}`;
|
||||||
@@ -300,16 +263,10 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||||||
await enablePage.enable(type, path);
|
await enablePage.enable(type, path);
|
||||||
|
|
||||||
this.server.post(configUrl(type, path), () => {
|
this.server.post(configUrl(type, path), () => {
|
||||||
assert.false(
|
throw new Error(`post request was made to config/root when it should not have been.`);
|
||||||
true,
|
|
||||||
'post request was made to config/root when no data was changed. test should fail.'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
this.server.post(configUrl('aws-lease', path), () => {
|
this.server.post(configUrl('aws-lease', path), () => {
|
||||||
assert.false(
|
throw new Error(`post request was made to config/lease when it should not have been.`);
|
||||||
true,
|
|
||||||
'post request was made to config/lease when no data was changed. test should fail.'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await click(SES.configTab);
|
await click(SES.configTab);
|
||||||
@@ -350,6 +307,35 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||||||
// cleanup
|
// cleanup
|
||||||
await runCmd(`delete sys/mounts/${path}`);
|
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) {
|
module('isCommunity', function (hooks) {
|
||||||
@@ -377,5 +363,97 @@ module('Acceptance | aws | configuration', function (hooks) {
|
|||||||
// cleanup
|
// cleanup
|
||||||
await runCmd(`delete sys/mounts/${path}`);
|
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',
|
environment: 'AZUREPUBLICCLOUD',
|
||||||
};
|
};
|
||||||
this.server.get(`${path}/config`, () => {
|
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 } };
|
return { data: { id: path, type: this.type, ...azureAccountAttrs } };
|
||||||
});
|
});
|
||||||
await enablePage.enable(this.type, path);
|
await enablePage.enable(this.type, path);
|
||||||
@@ -109,17 +109,6 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
|||||||
// cleanup
|
// cleanup
|
||||||
await runCmd(`delete sys/mounts/${path}`);
|
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 () {
|
module('create', function () {
|
||||||
@@ -129,9 +118,8 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
|||||||
await enablePage.enable(this.type, path);
|
await enablePage.enable(this.type, path);
|
||||||
|
|
||||||
this.server.post('/identity/oidc/config', () => {
|
this.server.post('/identity/oidc/config', () => {
|
||||||
assert.notOk(
|
throw new Error(
|
||||||
true,
|
`Request was made to return the issuer when it should not have been because user is on CE.`
|
||||||
'post request was made to issuer endpoint when on community and data not changed. test should fail.'
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -226,6 +214,42 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
|||||||
await runCmd(`delete sys/mounts/${path}`);
|
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) {
|
module('isEnterprise', function (hooks) {
|
||||||
@@ -245,7 +269,7 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
|||||||
environment: 'AZUREPUBLICCLOUD',
|
environment: 'AZUREPUBLICCLOUD',
|
||||||
};
|
};
|
||||||
this.server.get(`${path}/config`, () => {
|
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 } };
|
return { data: { id: path, type: this.type, ...wifAttrs } };
|
||||||
});
|
});
|
||||||
await enablePage.enable(this.type, path);
|
await enablePage.enable(this.type, path);
|
||||||
@@ -268,11 +292,11 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
|||||||
const path = `azure-${this.uid}`;
|
const path = `azure-${this.uid}`;
|
||||||
this.server.get(`${path}/config`, (schema, req) => {
|
this.server.get(`${path}/config`, (schema, req) => {
|
||||||
const payload = JSON.parse(req.requestBody);
|
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 } };
|
return { data: { id: path, type: this.type, attributes: payload } };
|
||||||
});
|
});
|
||||||
this.server.get(`identity/oidc/config`, () => {
|
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 createConfig(this.store, path, this.type); // create the azure account config in the store
|
||||||
await enablePage.enable(this.type, path);
|
await enablePage.enable(this.type, path);
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ module('Acceptance | GCP | configuration', function (hooks) {
|
|||||||
ttl: 3600,
|
ttl: 3600,
|
||||||
};
|
};
|
||||||
this.server.get(`${path}/config`, () => {
|
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 } };
|
return { data: { id: path, type: this.type, ...wifAttrs } };
|
||||||
});
|
});
|
||||||
await enablePage.enable(this.type, path);
|
await enablePage.enable(this.type, path);
|
||||||
@@ -99,7 +99,7 @@ module('Acceptance | GCP | configuration', function (hooks) {
|
|||||||
max_ttl: '4 hours',
|
max_ttl: '4 hours',
|
||||||
};
|
};
|
||||||
this.server.get(`${path}/config`, () => {
|
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 } };
|
return { data: { id: path, type: this.type, ...GCPAccountAttrs } };
|
||||||
});
|
});
|
||||||
await enablePage.enable(this.type, path);
|
await enablePage.enable(this.type, path);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
export const SECRET_ENGINE_SELECTORS = {
|
export const SECRET_ENGINE_SELECTORS = {
|
||||||
configTab: '[data-test-configuration-tab]',
|
configTab: '[data-test-configuration-tab]',
|
||||||
configure: '[data-test-secret-backend-configure]',
|
configure: '[data-test-secret-backend-configure]',
|
||||||
|
configureNote: (name: string) => `[data-test-configure-note="${name}"]`,
|
||||||
configureTitle: (type: string) => `[data-test-backend-configure-title="${type}"]`,
|
configureTitle: (type: string) => `[data-test-backend-configure-title="${type}"]`,
|
||||||
configurationToggle: '[data-test-mount-config-toggle]',
|
configurationToggle: '[data-test-mount-config-toggle]',
|
||||||
createSecret: '[data-test-secret-create]',
|
createSecret: '[data-test-secret-create]',
|
||||||
@@ -24,6 +25,7 @@ export const SECRET_ENGINE_SELECTORS = {
|
|||||||
viewBackend: '[data-test-backend-view-link]',
|
viewBackend: '[data-test-backend-view-link]',
|
||||||
warning: '[data-test-warning]',
|
warning: '[data-test-warning]',
|
||||||
configureForm: '[data-test-configure-form]',
|
configureForm: '[data-test-configure-form]',
|
||||||
|
additionalConfigModelTitle: '[data-test-additional-config-model-title]',
|
||||||
wif: {
|
wif: {
|
||||||
accessTypeSection: '[data-test-access-type-section]',
|
accessTypeSection: '[data-test-access-type-section]',
|
||||||
accessTitle: '[data-test-access-title]',
|
accessTitle: '[data-test-access-title]',
|
||||||
@@ -35,8 +37,6 @@ export const SECRET_ENGINE_SELECTORS = {
|
|||||||
issuerWarningSave: '[data-test-issuer-save]',
|
issuerWarningSave: '[data-test-issuer-save]',
|
||||||
},
|
},
|
||||||
aws: {
|
aws: {
|
||||||
rootForm: '[data-test-root-form]',
|
|
||||||
leaseTitle: '[data-test-lease-title]',
|
|
||||||
deleteRole: (role: string) => `[data-test-aws-role-delete="${role}"]`,
|
deleteRole: (role: string) => `[data-test-aws-role-delete="${role}"]`,
|
||||||
},
|
},
|
||||||
ssh: {
|
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