mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 01:32:33 +00:00
WIF sidebranch (#28148)
* manual cherry pick to deal with all the merge things * changelog * test fixes * Update 28148.txt * fix tests failures after main merge * fix test failures after main merge * Add Access Type and conditionally render WIF fields (#28149) * initial work. * remove access_type * better no model logic well kind of * rollback attrs * remove defaults * stopping point * wip changing back to sidebranch * hustling shuffling and serializing * some of the component test coverage * disable acces type if editing * test coverage * hide max retries that sneaky bugger * cleanup * cleanup * Update root-config.js * remove flash message check, locally passes great but on ci flaky * clean up * thank you chelsea * test clean up per enterprise vs community * address pr comments * welp a miss add * UI (sidebranch) WIF Issuer field (#28187) * Add type declaration files for aws config models * use updated task syntax for save method on configure-aws * fix types on edit route * fetch issuer on configure edit page if aws + enterprise * track issuer within configure-aws component * add placeholder support on form-field * Add warning if issuer changed from previous value or could not be read * cleanup * preliminary tests * dont use while loop so we can test the modal * tests * cleanup * fix tests * remove extra tracked value and duplicate changed attrs check * modal footer --------- Co-authored-by: Angel Garbarino <argarbarino@gmail.com> * Display issuer on Configuration details (#28209) * display issuer on configuration details * workflow complete, now on to testing * handle issuer things * fix all the broken tests things * add test coveragE: * cleanup * rename model/adapter * Update configure-aws.ts * Update aws-configuration-test.js * 90 percent there for pr comments * last one for tonight * a few more because why not * hasDirtyAttributes fixes * revert back to previous noRead->queryIssuerError --------- Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
This commit is contained in:
3
changelog/28148.txt
Normal file
3
changelog/28148.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:feature
|
||||
**Feature Name**: Add WIF fields to AWS secrets engine.
|
||||
```
|
||||
38
ui/app/adapters/identity/oidc/config.js
Normal file
38
ui/app/adapters/identity/oidc/config.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ApplicationAdapter from '../../application';
|
||||
|
||||
export default class IdentityOidcConfig extends ApplicationAdapter {
|
||||
namespace = 'v1';
|
||||
|
||||
queryRecord() {
|
||||
return this.ajax(`${this.buildURL()}/identity/oidc/config`, 'GET').then((resp) => {
|
||||
return {
|
||||
...resp,
|
||||
id: 'identity-oidc-config', // id required for ember data. only one record is expected so static id is fine
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
createOrUpdate(store, type, snapshot) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot);
|
||||
return this.ajax(`${this.buildURL()}/identity/oidc/config`, 'POST', { data }).then((resp) => {
|
||||
// id is returned from API so we do not need to explicitly set it here
|
||||
return {
|
||||
...resp,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
createRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
}
|
||||
|
||||
updateRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
}
|
||||
}
|
||||
6
ui/app/components/modal-form/oidc-key-template.hbs
Normal file
6
ui/app/components/modal-form/oidc-key-template.hbs
Normal file
@@ -0,0 +1,6 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<Oidc::KeyForm @onSave={{this.onSave}} @model={{this.key}} @onCancel={{@onCancel}} @isModalForm={{true}} />
|
||||
41
ui/app/components/modal-form/oidc-key-template.js
Normal file
41
ui/app/components/modal-form/oidc-key-template.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
/**
|
||||
* @module ModalForm::OidcKeyTemplate
|
||||
* ModalForm::OidcKeyTemplate components render within a modal and create a model using the input from the search select. The model is passed to the oidc/key-form.
|
||||
*
|
||||
* @example
|
||||
* <ModalForm::OidcKeyTemplate
|
||||
* @nameInput="new-key-name"
|
||||
* @onSave={{this.closeModal}}
|
||||
* @onCancel={{@onCancel}}
|
||||
* />
|
||||
*
|
||||
* @callback onCancel - callback triggered when cancel button is clicked
|
||||
* @callback onSave - callback triggered when save button is clicked
|
||||
* @param {string} nameInput - the name of the newly created key
|
||||
*/
|
||||
|
||||
export default class OidcKeyTemplate extends Component {
|
||||
@service store;
|
||||
@tracked key = null; // model record passed to oidc/key-form
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.key = this.store.createRecord('oidc/key', { name: this.args.nameInput });
|
||||
}
|
||||
|
||||
@action onSave(keyModel) {
|
||||
this.args.onSave(keyModel);
|
||||
// Reset component key for next use
|
||||
this.key = null;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,23 @@
|
||||
@modelValidations={{this.modelValidations}}
|
||||
@onKeyUp={{this.onKeyUp}}
|
||||
/>
|
||||
<FormFieldGroups @model={{@mountModel}} @renderGroup="Method Options" />
|
||||
|
||||
<FormFieldGroups @model={{@mountModel}} @renderGroup="Method Options">
|
||||
<:identityTokenKey>
|
||||
<SearchSelectWithModal
|
||||
@id="key"
|
||||
@fallbackComponent="input-search"
|
||||
@inputValue={{@mountModel.config.identityTokenKey}}
|
||||
@onChange={{this.handleIdentityTokenKeyChange}}
|
||||
@models={{array "oidc/key"}}
|
||||
@selectLimit="1"
|
||||
@modalFormTemplate="modal-form/oidc-key-template"
|
||||
@placeholder="Search for an existing OIDC key, or type a new key name to create it."
|
||||
@fallbackComponentPlaceholder="Input a key name"
|
||||
@modalSubtext="This key will be created in the OIDC key path."
|
||||
/>
|
||||
</:identityTokenKey>
|
||||
</FormFieldGroups>
|
||||
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
|
||||
@@ -190,4 +190,10 @@ export default class MountBackendForm extends Component<Args> {
|
||||
this.typeChangeSideEffect(value);
|
||||
this.checkPathChange(value);
|
||||
}
|
||||
|
||||
@action
|
||||
handleIdentityTokenKeyChange(value: string[] | string): void {
|
||||
// if array, it's coming from the search-select component, otherwise it hit the fallback component and will come in as a string.
|
||||
this.args.mountModel.config.identityTokenKey = Array.isArray(value) ? value[0] : value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { task } from 'ember-concurrency';
|
||||
* @param {Object} model - oidc client model
|
||||
* @param {onCancel} onCancel - callback triggered when cancel button is clicked
|
||||
* @param {onSave} onSave - callback triggered on save success
|
||||
* @param {boolean} [isModalForm=false] - if true, hides inputs related to selecting an application which is only relevant to the OIDC provider workflow.
|
||||
*/
|
||||
|
||||
export default class OidcKeyForm extends Component {
|
||||
@@ -83,7 +84,9 @@ export default class OidcKeyForm extends Component {
|
||||
`Successfully ${isNew ? 'created' : 'updated'} the key
|
||||
${name}.`
|
||||
);
|
||||
this.args.onSave();
|
||||
// this form is sometimes used in a modal, passing the model notifies
|
||||
// the parent if the save was successful
|
||||
this.args.onSave(this.args.model);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error.errors ? error.errors.join('. ') : error.message;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<form {{on "submit" (perform this.save)}} aria-label="save aws creds" data-test-root-form>
|
||||
<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}} />
|
||||
@@ -17,12 +17,64 @@
|
||||
Access to AWS
|
||||
</h2>
|
||||
<div class="box is-fullwidth is-sideless">
|
||||
<FormFieldGroups
|
||||
@model={{@rootConfig}}
|
||||
@mode={{if @rootConfig.isNew "create" "edit"}}
|
||||
@modelValidations={{this.modelValidationsRoot}}
|
||||
@useEnableInput={{true}}
|
||||
/>
|
||||
{{! 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">Workflow Identity Federation</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
{{/if}}
|
||||
{{#if (eq this.accessType "wif")}}
|
||||
{{! WIF Fields }}
|
||||
{{#each @issuerConfig.attrs 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 }}
|
||||
@@ -62,4 +114,23 @@
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
</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}}
|
||||
@@ -14,21 +14,24 @@ 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 Store from '@ember-data/store';
|
||||
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.
|
||||
* 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}}
|
||||
@leaseConfig={{this.model.aws-lease-config}}
|
||||
@backendPath={{this.model.id}}
|
||||
/>
|
||||
* ```
|
||||
*
|
||||
@@ -40,55 +43,112 @@ import type FlashMessageService from 'vault/services/flash-messages';
|
||||
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: Store;
|
||||
@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 = '';
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*save(event: Event) {
|
||||
event.preventDefault();
|
||||
this.resetErrors();
|
||||
const { leaseConfig, rootConfig } = this.args;
|
||||
// Note: aws/root-config model does not have any validations
|
||||
const isValid = this.validate(leaseConfig);
|
||||
if (!isValid) return;
|
||||
// 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.
|
||||
disableAccessType = false;
|
||||
|
||||
const leaseAttrChanged =
|
||||
Object.keys(leaseConfig.changedAttributes()).filter((item) => item !== 'backend').length > 0;
|
||||
const rootAttrChanged =
|
||||
Object.keys(rootConfig.changedAttributes()).filter((item) => item !== 'backend').length > 0;
|
||||
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 bet 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;
|
||||
}
|
||||
|
||||
if (!leaseAttrChanged && !rootAttrChanged) {
|
||||
this.flashMessages.info('No changes detected.');
|
||||
this.transition();
|
||||
}
|
||||
@action continueSubmitForm() {
|
||||
// called when the user confirms they are okay with the issuer change
|
||||
this.saveIssuerWarning = '';
|
||||
this.save.perform();
|
||||
}
|
||||
|
||||
const rootSaved = rootAttrChanged ? yield this.saveRoot() : false;
|
||||
const leaseSaved = leaseAttrChanged ? yield this.saveLease() : false;
|
||||
// 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();
|
||||
})
|
||||
);
|
||||
|
||||
if (rootSaved || leaseSaved) {
|
||||
this.transition();
|
||||
} else {
|
||||
// otherwise there was a failure and we should not transition and exit the function.
|
||||
return;
|
||||
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() {
|
||||
async saveRoot(): Promise<boolean> {
|
||||
const { backendPath, rootConfig } = this.args;
|
||||
try {
|
||||
await rootConfig.save();
|
||||
@@ -101,7 +161,7 @@ export default class ConfigureAwsComponent extends Component<Args> {
|
||||
}
|
||||
}
|
||||
|
||||
async saveLease() {
|
||||
async saveLease(): Promise<boolean> {
|
||||
const { backendPath, leaseConfig } = this.args;
|
||||
try {
|
||||
await leaseConfig.save();
|
||||
@@ -146,6 +206,22 @@ export default class ConfigureAwsComponent extends Component<Args> {
|
||||
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.
|
||||
|
||||
@@ -134,6 +134,13 @@ const MOUNTABLE_SECRET_ENGINES = [
|
||||
},
|
||||
];
|
||||
|
||||
// A list of Workflow Identity Federation engines. Will eventually include Azure and GCP.
|
||||
export const WIF_ENGINES = ['aws'];
|
||||
|
||||
export function wifEngines() {
|
||||
return WIF_ENGINES.slice();
|
||||
}
|
||||
|
||||
// Secret Engines that have their own configuration page and actions
|
||||
// These engines do not exist in their own Ember engine.
|
||||
// Ex: AWS vs. LDAP which is configurable but is handled inside the routing of its own Ember engine.
|
||||
|
||||
@@ -9,8 +9,33 @@ import { regions } from 'vault/helpers/aws-regions';
|
||||
|
||||
export default class AwsRootConfig extends Model {
|
||||
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
|
||||
|
||||
// IAM only fields
|
||||
@attr('string') accessKey;
|
||||
@attr('string', { sensitive: true }) secretKey; // obfuscated, never returned by API
|
||||
|
||||
// WIF only fields
|
||||
@attr('string', {
|
||||
label: 'Role ARN',
|
||||
subText: 'Role ARN to assume for plugin workload identity federation.',
|
||||
})
|
||||
roleArn;
|
||||
@attr('string', {
|
||||
subText:
|
||||
'The audience claim value for plugin identity tokens. Must match an allowed audience configured for the target IAM OIDC identity provider.',
|
||||
})
|
||||
identityTokenAudience;
|
||||
@attr({
|
||||
label: 'Identity token TTL',
|
||||
helperTextDisabled:
|
||||
'The TTL of generated tokens. Defaults to 1 hour, turn on the toggle to specify a different value.',
|
||||
helperTextEnabled: 'The TTL of generated tokens.',
|
||||
subText: '',
|
||||
editType: 'ttl',
|
||||
})
|
||||
identityTokenTtl;
|
||||
|
||||
// Fields that show regardless of access type
|
||||
@attr('string', {
|
||||
possibleValues: regions(),
|
||||
subText:
|
||||
@@ -21,27 +46,45 @@ export default class AwsRootConfig extends Model {
|
||||
iamEndpoint;
|
||||
@attr('string', { label: 'STS endpoint' }) stsEndpoint;
|
||||
@attr('number', {
|
||||
defaultValue: -1,
|
||||
label: 'Maximum retries',
|
||||
subText: 'Number of max retries the client should use for recoverable errors. Default is -1.',
|
||||
})
|
||||
maxRetries;
|
||||
// there are more options available on the API, but the UI does not support them yet.
|
||||
|
||||
get attrs() {
|
||||
const keys = ['accessKey', 'region', 'iamEndpoint', 'stsEndpoint', 'maxRetries'];
|
||||
const keys = [
|
||||
'roleArn',
|
||||
'identityTokenAudience',
|
||||
'identityTokenTtl',
|
||||
'accessKey',
|
||||
'region',
|
||||
'iamEndpoint',
|
||||
'stsEndpoint',
|
||||
'maxRetries',
|
||||
];
|
||||
return expandAttributeMeta(this, keys);
|
||||
}
|
||||
|
||||
get formFieldGroups() {
|
||||
return [
|
||||
{ default: ['accessKey', 'secretKey'] },
|
||||
{
|
||||
'Root config options': ['region', 'iamEndpoint', 'stsEndpoint', 'maxRetries'],
|
||||
},
|
||||
];
|
||||
// "filedGroupsWif" and "fieldGroupsIam" are passed to the FormFieldGroups component to determine which group to show in the form (ex: @groupName="fieldGroupsWif")
|
||||
get fieldGroupsWif() {
|
||||
return fieldToAttrs(this, this.formFieldGroups('wif'));
|
||||
}
|
||||
|
||||
get fieldGroups() {
|
||||
return fieldToAttrs(this, this.formFieldGroups);
|
||||
get fieldGroupsIam() {
|
||||
return fieldToAttrs(this, this.formFieldGroups('iam'));
|
||||
}
|
||||
|
||||
formFieldGroups(accessType = 'iam') {
|
||||
const formFieldGroups = [];
|
||||
if (accessType === 'wif') {
|
||||
formFieldGroups.push({ default: ['roleArn', 'identityTokenAudience', 'identityTokenTtl'] });
|
||||
}
|
||||
if (accessType === 'iam') {
|
||||
formFieldGroups.push({ default: ['accessKey', 'secretKey'] });
|
||||
}
|
||||
formFieldGroups.push({
|
||||
'Root config options': ['region', 'iamEndpoint', 'stsEndpoint', 'maxRetries'],
|
||||
});
|
||||
return formFieldGroups;
|
||||
}
|
||||
}
|
||||
|
||||
23
ui/app/models/identity/oidc/config.js
Normal file
23
ui/app/models/identity/oidc/config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
|
||||
export default class IdentityOidcConfig extends Model {
|
||||
@attr('string', {
|
||||
label: 'Issuer',
|
||||
subText:
|
||||
"The Issuer URL to be used in configuring Vault as an identity provider in AWS. If not set, Vault's default issuer will be used",
|
||||
docLink: '/vault/api-docs/secret/identity/tokens#configure-the-identity-tokens-backend',
|
||||
placeholder: 'https://vault.prod/v1/identity/oidc',
|
||||
})
|
||||
issuer;
|
||||
|
||||
get attrs() {
|
||||
const keys = ['issuer'];
|
||||
return expandAttributeMeta(this, keys);
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,14 @@ export default class MountConfigModel extends Model {
|
||||
})
|
||||
pluginVersion;
|
||||
|
||||
// identityTokenKey is yielded in a named block on the mount-backend-form component
|
||||
@attr({
|
||||
label: 'Identity token key',
|
||||
subText: `A named key to sign tokens. If not provided, this will default to Vault's OIDC default key.`,
|
||||
editType: 'yield',
|
||||
})
|
||||
identityTokenKey;
|
||||
|
||||
// Auth mount userLockoutConfig params, added to user_lockout_config object in saveModel method
|
||||
@attr('string', {
|
||||
label: 'Lockout threshold',
|
||||
|
||||
@@ -179,6 +179,10 @@ export default class SecretEngineModel extends Model {
|
||||
if (type === 'kv' && parseInt(this.version, 10) === 2) {
|
||||
fields.push('casRequired', 'deleteVersionAfter', 'maxVersions');
|
||||
}
|
||||
// WIF secret engines
|
||||
if (type === 'aws') {
|
||||
fields.push('config.identityTokenKey');
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
@@ -228,6 +232,17 @@ export default class SecretEngineModel extends Model {
|
||||
// no ttl options for keymgmt
|
||||
optionFields = [...CORE_OPTIONS, 'config.allowedManagedKeys', ...STANDARD_CONFIG];
|
||||
break;
|
||||
case 'aws':
|
||||
defaultFields = ['path'];
|
||||
optionFields = [
|
||||
...CORE_OPTIONS,
|
||||
'config.defaultLeaseTtl',
|
||||
'config.maxLeaseTtl',
|
||||
'config.identityTokenKey',
|
||||
'config.allowedManagedKeys',
|
||||
...STANDARD_CONFIG,
|
||||
];
|
||||
break;
|
||||
default:
|
||||
defaultFields = ['path'];
|
||||
optionFields = [
|
||||
|
||||
@@ -13,6 +13,7 @@ import { action } from '@ember/object';
|
||||
|
||||
import type Store from '@ember-data/store';
|
||||
import type SecretEngineModel from 'vault/models/secret-engine';
|
||||
import type VersionService from 'vault/services/version';
|
||||
|
||||
// This route file is reused for all configurable secret engines.
|
||||
// It generates config models based on the engine type.
|
||||
@@ -25,11 +26,12 @@ const CONFIG_ADAPTERS_PATHS: Record<string, string[]> = {
|
||||
|
||||
export default class SecretsBackendConfigurationEdit extends Route {
|
||||
@service declare readonly store: Store;
|
||||
@service declare readonly version: VersionService;
|
||||
|
||||
async model() {
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
const secretEngineRecord = this.modelFor('vault.cluster.secrets.backend') as { type: SecretEngineModel };
|
||||
const type = secretEngineRecord.type as string;
|
||||
const secretEngineRecord = this.modelFor('vault.cluster.secrets.backend') as SecretEngineModel;
|
||||
const type = secretEngineRecord.type;
|
||||
|
||||
// if the engine type is not configurable, return a 404.
|
||||
if (!secretEngineRecord || !CONFIGURABLE_SECRET_ENGINES.includes(type)) {
|
||||
@@ -66,6 +68,17 @@ export default class SecretsBackendConfigurationEdit extends Route {
|
||||
}
|
||||
}
|
||||
}
|
||||
// if the type is AWS and it's enterprise, we also fetch the issuer
|
||||
// from a global endpoint which has no associated model/adapter
|
||||
if (type === 'aws' && this.version.isEnterprise) {
|
||||
try {
|
||||
const response = await this.store.queryRecord('identity/oidc/config', {});
|
||||
model['identity-oidc-config'] = response;
|
||||
} catch (e) {
|
||||
// return a property called queryIssuerError and let the component handle it.
|
||||
model['identity-oidc-config'] = { queryIssuerError: true };
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { reject } from 'rsvp';
|
||||
|
||||
export default class SecretsBackendConfigurationRoute extends Route {
|
||||
@service store;
|
||||
@service version;
|
||||
|
||||
async model() {
|
||||
const secretEngineModel = this.modelFor('vault.cluster.secrets.backend');
|
||||
@@ -74,11 +75,19 @@ export default class SecretsBackendConfigurationRoute extends Route {
|
||||
}
|
||||
|
||||
async fetchAwsConfigs(id) {
|
||||
// AWS has two configuration endpoints root and lease, return an array of these responses.
|
||||
// AWS has two configuration endpoints root and lease, as well as a separate endpoint for the issuer.
|
||||
// return an array of these responses.
|
||||
const configArray = [];
|
||||
const configRoot = await this.fetchAwsConfig(id, 'aws/root-config');
|
||||
const configLease = await this.fetchAwsConfig(id, 'aws/lease-config');
|
||||
configArray.push(configRoot, configLease);
|
||||
let issuer = null;
|
||||
if (this.version.isEnterprise && configRoot) {
|
||||
// Issuer is an enterprise only related feature
|
||||
// Issuer is also a global endpoint that doesn't mean anything in the AWS secret details context if WIF related fields on the rootConfig have not been set.
|
||||
const WIF_FIELDS = ['roleArn', 'identityTokenAudience', 'identityTokenTtl'];
|
||||
WIF_FIELDS.some((field) => configRoot[field]) ? (issuer = await this.fetchIssuer()) : null;
|
||||
}
|
||||
configArray.push(configRoot, configLease, issuer);
|
||||
return configArray;
|
||||
}
|
||||
|
||||
@@ -94,6 +103,15 @@ export default class SecretsBackendConfigurationRoute extends Route {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchIssuer() {
|
||||
try {
|
||||
return await this.store.queryRecord('identity/oidc/config', {});
|
||||
} catch (e) {
|
||||
// silently fail if the endpoint is not available or the user doesn't have permission to access it.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSshCaConfig(id) {
|
||||
try {
|
||||
return await this.store.queryRecord('ssh/ca-config', { backend: id });
|
||||
|
||||
29
ui/app/serializers/aws/root-config.js
Normal file
29
ui/app/serializers/aws/root-config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from '../application';
|
||||
|
||||
export default class AwsRootConfigSerializer extends ApplicationSerializer {
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
if (!payload.data) {
|
||||
return super.normalizeResponse(...arguments);
|
||||
}
|
||||
// remove identityTokenTtl and maxRetries if the API's default value of 0 or -1, respectively. We don't want to display this value on configuration details if they haven't changed the default value
|
||||
if (payload.data.identity_token_ttl === 0) {
|
||||
delete payload.data.identity_token_ttl;
|
||||
}
|
||||
if (payload.data.max_retries === -1) {
|
||||
delete payload.data.max_retries;
|
||||
}
|
||||
const normalizedPayload = {
|
||||
id: payload.id,
|
||||
backend: payload.backend,
|
||||
data: {
|
||||
...payload.data,
|
||||
},
|
||||
};
|
||||
return super.normalizeResponse(store, primaryModelClass, normalizedPayload, id, requestType);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
import { WIF_ENGINES } from 'vault/helpers/mountable-secret-engines';
|
||||
|
||||
export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
|
||||
attrs: {
|
||||
@@ -84,6 +85,13 @@ export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
|
||||
data.options = data.version ? { version: data.version } : {};
|
||||
delete data.version;
|
||||
|
||||
if (!WIF_ENGINES.includes(type)) {
|
||||
// only send identity_token_key if it's set on a WIF secret engine.
|
||||
// because of issues with the model unloading with a belongsTo relationships
|
||||
// identity_token_key can accidentally carry over if a user backs out of the form and changes the type from WIF to non-WIF.
|
||||
delete data.config.identity_token_key;
|
||||
}
|
||||
|
||||
if (type !== 'kv' || data.options.version === 1) {
|
||||
// These items are on the model, but used by the kv-v2 config endpoint only
|
||||
delete data.max_versions;
|
||||
|
||||
@@ -10,47 +10,49 @@
|
||||
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
{{! RADIO CARD + SEARCH SELECT }}
|
||||
<div class="box is-sideless is-fullwidth is-marginless has-top-padding-xxl">
|
||||
<Hds::Text::Display @tag="h2" @size="400">Allowed applications</Hds::Text::Display>
|
||||
<div class="is-flex-row">
|
||||
<RadioCard
|
||||
data-test-oidc-radio="allow-all"
|
||||
@title="Allow every application to use"
|
||||
@description="All applications can use this key for authentication requests."
|
||||
@icon="globe"
|
||||
@value="allow_all"
|
||||
@groupValue={{this.radioCardGroupValue}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
/>
|
||||
<RadioCard
|
||||
data-test-oidc-radio="limited"
|
||||
@title="Limit access to selected application"
|
||||
@description="Only selected applications can use this key for authentication requests."
|
||||
@icon="globe-private"
|
||||
@value="limited"
|
||||
@groupValue={{this.radioCardGroupValue}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
@disabled={{@model.isNew}}
|
||||
@disabledTooltipMessage="This option has been disabled for now. To limit access, you must first create an application that references this key."
|
||||
/>
|
||||
{{#unless @isModalForm}}
|
||||
{{! RADIO CARD + SEARCH SELECT }}
|
||||
<div class="box is-sideless is-fullwidth is-marginless has-top-padding-xxl">
|
||||
<Hds::Text::Display @tag="h2" @size="400">Allowed applications</Hds::Text::Display>
|
||||
<div class="is-flex-row">
|
||||
<RadioCard
|
||||
data-test-oidc-radio="allow-all"
|
||||
@title="Allow every application to use"
|
||||
@description="All applications can use this key for authentication requests."
|
||||
@icon="globe"
|
||||
@value="allow_all"
|
||||
@groupValue={{this.radioCardGroupValue}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
/>
|
||||
<RadioCard
|
||||
data-test-oidc-radio="limited"
|
||||
@title="Limit access to selected application"
|
||||
@description="Only selected applications can use this key for authentication requests."
|
||||
@icon="globe-private"
|
||||
@value="limited"
|
||||
@groupValue={{this.radioCardGroupValue}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
@disabled={{@model.isNew}}
|
||||
@disabledTooltipMessage="This option has been disabled for now. To limit access, you must first create an application that references this key."
|
||||
/>
|
||||
</div>
|
||||
{{#if (eq this.radioCardGroupValue "limited")}}
|
||||
<SearchSelect
|
||||
@id="allowedClientIds"
|
||||
@label="Application name"
|
||||
@subText="Select which applications are allowed to use this key. Only applications that currently reference this key will appear in the dropdown."
|
||||
@models={{array "oidc/client"}}
|
||||
@inputValue={{@model.allowedClientIds}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="string-list"
|
||||
@passObject={{true}}
|
||||
@objectKeys={{array "clientId"}}
|
||||
@queryObject={{this.filterDropdownOptions}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if (eq this.radioCardGroupValue "limited")}}
|
||||
<SearchSelect
|
||||
@id="allowedClientIds"
|
||||
@label="Application name"
|
||||
@subText="Select which applications are allowed to use this key. Only applications that currently reference this key will appear in the dropdown."
|
||||
@models={{array "oidc/client"}}
|
||||
@inputValue={{@model.allowedClientIds}}
|
||||
@onChange={{this.handleClientSelection}}
|
||||
@disallowNewItems={{true}}
|
||||
@fallbackComponent="string-list"
|
||||
@passObject={{true}}
|
||||
@objectKeys={{array "clientId"}}
|
||||
@queryObject={{this.filterDropdownOptions}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
<div class="field box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<Hds::Button
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<SecretEngine::ConfigureAws
|
||||
@leaseConfig={{this.model.aws-lease-config}}
|
||||
@rootConfig={{this.model.aws-root-config}}
|
||||
@issuerConfig={{this.model.identity-oidc-config}}
|
||||
@backendPath={{this.model.id}}
|
||||
/>
|
||||
{{else if (eq this.model.type "ssh")}}
|
||||
|
||||
@@ -65,7 +65,11 @@
|
||||
@onKeyUp={{@onKeyUp}}
|
||||
@modelValidations={{@modelValidations}}
|
||||
@showHelpText={{@showHelpText}}
|
||||
/>
|
||||
>
|
||||
{{#if (and (has-block "identityTokenKey") (eq attr.name "identityTokenKey"))}}
|
||||
{{yield to="identityTokenKey"}}
|
||||
{{/if}}
|
||||
</FormField>
|
||||
{{else}}
|
||||
{{! OTHERWISE WE'RE EDITING }}
|
||||
{{#if (or (eq attr.name "name") (eq attr.options.editDisabled true))}}
|
||||
|
||||
@@ -322,6 +322,7 @@
|
||||
disabled={{and @attr.options.editDisabled (not @model.isNew)}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
placeholder={{@attr.options.placeholder}}
|
||||
value={{get @model this.valuePath}}
|
||||
{{on "change" this.onChangeWithEvent}}
|
||||
{{on "input" this.onChangeWithEvent}}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
{{on "keyup" this.inputChanged}}
|
||||
placeholder={{@placeholder}}
|
||||
autocomplete="off"
|
||||
data-test-input-search={{@id}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,8 +18,9 @@
|
||||
onChange=@onChange
|
||||
inputValue=@inputValue
|
||||
helpText=@helpText
|
||||
placeholder=@placeholder
|
||||
placeholder=(or @fallbackComponentPlaceholder @placeholder)
|
||||
id=@id
|
||||
selectLimit=@selectLimit
|
||||
}}
|
||||
{{else}}
|
||||
{{#if @label}}
|
||||
|
||||
@@ -38,7 +38,9 @@ import { addToArray } from 'vault/helpers/add-to-array';
|
||||
* @param {string} [subText] - Text to be displayed below the label
|
||||
* @param {string} fallbackComponent - name of component to be rendered if the API call 403s
|
||||
* @param {string} [placeholder] - placeholder text to override the default text of "Search"
|
||||
* @param {string} [fallbackComponentPlaceholder] - specific placeholder text relevant to fallback component. In some cases, the placeholder text does not make sense for both the search-select and the fallback component. Ex: "Input key name" for input-search and "Search or type to create a new item" for search-select.
|
||||
* @param {boolean} [displayInherit=false] - if you need the search select component to display inherit instead of box.
|
||||
* @param {number} [selectLimit=1] - if you only want the user to select a limited number of options, add number to this param.
|
||||
*/
|
||||
export default class SearchSelectWithModal extends Component {
|
||||
@service store;
|
||||
|
||||
@@ -40,6 +40,7 @@ export default class SecretsEngineMountConfig extends Component<Args> {
|
||||
{ label: 'Seal Wrap', value: model.sealWrap },
|
||||
{ label: 'Default Lease TTL', value: duration([model.config.defaultLeaseTtl]) },
|
||||
{ label: 'Max Lease TTL', value: duration([model.config.maxLeaseTtl]) },
|
||||
{ label: 'Identity Token Key', value: model.config.identityTokenKey },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,218 +33,352 @@ module('Acceptance | aws | configuration', function (hooks) {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.flashSuccessSpy = spy(flash, 'success');
|
||||
this.flashInfoSpy = spy(flash, 'info');
|
||||
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.uid = uuidv4();
|
||||
return authPage.login();
|
||||
});
|
||||
|
||||
test('it should prompt configuration after mounting the aws engine', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
// in this test go through the full mount process. Bypass this step in later tests.
|
||||
await visit('/vault/settings/mount-secret-backend');
|
||||
await click(SES.mountType('aws'));
|
||||
await fillIn(GENERAL.inputByAttr('path'), path);
|
||||
await click(SES.mountSubmit);
|
||||
await click(SES.configTab);
|
||||
assert.dom(GENERAL.emptyStateTitle).hasText('AWS not configured');
|
||||
assert.dom(GENERAL.emptyStateActions).hasText('Configure AWS');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should transition to configure page on click "Configure" from toolbar', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
assert.strictEqual(currentURL(), `/vault/secrets/${path}/configuration/edit`);
|
||||
assert.dom(SES.configureTitle('aws')).hasText('Configure AWS');
|
||||
assert.dom(SES.aws.rootForm).exists('it lands on the root configuration form.');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should show error if old url is entered', async function (assert) {
|
||||
// we are intentionally not redirecting from the old url to the new one.
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
await click(SES.configTab);
|
||||
await visit(`/vault/settings/secrets/configure/${path}`);
|
||||
assert.dom(GENERAL.notFound).exists('shows page-error');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should save root AWS configuration', async function (assert) {
|
||||
assert.expect(3);
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
|
||||
this.server.post(configUrl('aws-lease', path), () => {
|
||||
assert.false(true, 'post request was made to config/lease when no data was changed. test should fail.');
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig();
|
||||
await click(SES.aws.save);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s root configuration.`),
|
||||
'Success flash message is rendered showing the root configuration was saved.'
|
||||
);
|
||||
assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', 'Access Key has been set.');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Secret key'))
|
||||
.doesNotExist('Secret key is not shown because it does not get returned by the api.');
|
||||
// cleanup
|
||||
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.');
|
||||
test('it should prompt configuration after mounting the aws engine', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
// in this test go through the full mount process. Bypass this step in later tests.
|
||||
await visit('/vault/settings/mount-secret-backend');
|
||||
await click(SES.mountType('aws'));
|
||||
await fillIn(GENERAL.inputByAttr('path'), path);
|
||||
await click(SES.mountSubmit);
|
||||
await click(SES.configTab);
|
||||
assert.dom(GENERAL.emptyStateTitle).hasText('AWS not configured');
|
||||
assert.dom(GENERAL.emptyStateActions).hasText('Configure AWS');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig(false, false, true); // only fills in lease config with defaults
|
||||
await click(SES.aws.save);
|
||||
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('33s', `Default TTL has been set.`);
|
||||
assert.dom(GENERAL.infoRowValue('Max Lease TTL')).hasText('44s', `Max lease TTL has been set.`);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it shows AWS mount configuration details', async function (assert) {
|
||||
assert.expect(12);
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
this.server.get(`${path}/config/root`, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.ok(true, 'request made to config/root when navigating to the configuration page.');
|
||||
return { data: { id: path, type, attributes: payload } };
|
||||
test('it should transition to configure page on click "Configure" from toolbar', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
assert.strictEqual(currentURL(), `/vault/secrets/${path}/configuration/edit`);
|
||||
assert.dom(SES.configureTitle('aws')).hasText('Configure AWS');
|
||||
assert.dom(SES.aws.rootForm).exists('it lands on the root configuration form.');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
await enablePage.enable(type, path);
|
||||
createConfig(this.store, path, type); // create the aws root config in the store
|
||||
await click(SES.configTab);
|
||||
for (const key of expectedConfigKeys(type)) {
|
||||
assert.dom(GENERAL.infoRowLabel(key)).exists(`${key} on the ${type} config details exists.`);
|
||||
const responseKeyAndValue = expectedValueOfConfigKeys(type, key);
|
||||
|
||||
test('it should show error if old url is entered', async function (assert) {
|
||||
// we are intentionally not redirecting from the old url to the new one.
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
await click(SES.configTab);
|
||||
await visit(`/vault/settings/secrets/configure/${path}`);
|
||||
assert.dom(GENERAL.notFound).exists('shows page-error');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should save root AWS—with WIF options—configuration', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
|
||||
this.server.post(configUrl('aws-lease', path), () => {
|
||||
assert.false(
|
||||
true,
|
||||
'post request was made to config/lease when no data was changed. test should fail.'
|
||||
);
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig('withWif');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.aws.issuerWarningModal).exists('issue warning modal exists');
|
||||
await click(SES.aws.issuerWarningSave);
|
||||
// three flash messages, the first is about mounting the engine, only care about the last two
|
||||
assert.strictEqual(
|
||||
this.flashSuccessSpy.args[1][0],
|
||||
`Successfully saved ${path}'s root configuration.`,
|
||||
'first flash message about the root config.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
this.flashSuccessSpy.args[2][0],
|
||||
'Issuer saved successfully',
|
||||
'second success message is about the issuer.'
|
||||
);
|
||||
assert.dom(GENERAL.infoRowValue('Issuer')).exists('Issuer has been set and is shown.');
|
||||
assert.dom(GENERAL.infoRowValue('Role ARN')).hasText('foo-role', 'Role ARN has been set.');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue(key))
|
||||
.hasText(responseKeyAndValue, `value for ${key} on the ${type} config details exists.`);
|
||||
}
|
||||
// check mount configuration details are present and accurate.
|
||||
await click(SES.configurationToggle);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Path'))
|
||||
.hasText(`${path}/`, 'mount path is displayed in the configuration details');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should update AWS configuration details after editing', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
await enablePage.enable(type, path);
|
||||
// create accessKey with value foo and confirm it shows up in the details page.
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig();
|
||||
await click(SES.aws.save);
|
||||
assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', 'Access key is foo');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Region'))
|
||||
.doesNotExist('Region has not been added therefor it does not show up on the details view.');
|
||||
// edit root config details and lease config details and confirm the configuration.index page is updated.
|
||||
await click(SES.configure);
|
||||
// edit root config details
|
||||
await fillIn(GENERAL.inputByAttr('accessKey'), 'not-foo');
|
||||
await click(GENERAL.toggleGroup('Root config options'));
|
||||
await fillIn(GENERAL.inputByAttr('region'), 'ap-southeast-2');
|
||||
// add lease config details
|
||||
await fillInAwsConfig(false, false, true); // only fills in lease config with defaults
|
||||
await click(SES.aws.save);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Access key'))
|
||||
.hasText('not-foo', 'Access key has been updated to not-foo');
|
||||
assert.dom(GENERAL.infoRowValue('Region')).hasText('ap-southeast-2', 'Region has been added');
|
||||
assert.dom(GENERAL.infoRowValue('Default Lease TTL')).hasText('33s', 'Default Lease TTL has been added');
|
||||
assert.dom(GENERAL.infoRowValue('Max Lease TTL')).hasText('44s', 'Max Lease TTL has been added');
|
||||
// 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');
|
||||
});
|
||||
|
||||
test('it should not make a post request if lease or root data was unchanged', async function (assert) {
|
||||
assert.expect(3);
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
await enablePage.enable(type, path);
|
||||
|
||||
this.server.post(configUrl(type, path), () => {
|
||||
assert.false(true, 'post request was made to config/root when no data was changed. test should fail.');
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', path), () => {
|
||||
assert.false(true, 'post request was made to config/lease when no data was changed. test should fail.');
|
||||
.dom(GENERAL.infoRowValue('Identity token audience'))
|
||||
.hasText('foo-audience', 'Identity token audience has been set.');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Identity token TTL'))
|
||||
.hasText('7200', 'Identity token TTL has been set.');
|
||||
assert.dom(GENERAL.infoRowValue('Access key')).doesNotExist('Access key—a non-wif attr is not shown.');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await click(SES.aws.save);
|
||||
assert.true(
|
||||
this.flashInfoSpy.calledWith('No changes detected.'),
|
||||
'Flash message shows no changes detected.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/configuration`,
|
||||
'navigates back to the configuration index view'
|
||||
);
|
||||
assert.dom(GENERAL.emptyStateTitle).hasText('AWS not configured');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
test('it should not show issuer if no root WIF configuration data is returned', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
this.server.get(`${path}/config/root`, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.ok(true, 'request made to config/root when navigating to the configuration page.');
|
||||
return { data: { id: path, type, attributes: payload } };
|
||||
});
|
||||
this.server.get(`identity/oidc/config`, () => {
|
||||
assert.false(true, 'request made to return issuer. test should fail.');
|
||||
});
|
||||
await enablePage.enable(type, path);
|
||||
createConfig(this.store, path, type); // create the aws root config in the store
|
||||
await click(SES.configTab);
|
||||
assert.dom(GENERAL.infoRowLabel('Issuer')).doesNotExist(`Issuer does not exists on config details.`);
|
||||
assert.dom(GENERAL.infoRowLabel('Access key')).exists(`Access key does exists on config details.`);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should save root AWS—with IAM options—configuration', async function (assert) {
|
||||
assert.expect(3);
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
|
||||
this.server.post(configUrl('aws-lease', path), () => {
|
||||
assert.false(
|
||||
true,
|
||||
'post request was made to config/lease when no data was changed. test should fail.'
|
||||
);
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig('withAccess');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s root configuration.`),
|
||||
'Success flash message is rendered showing the root configuration was saved.'
|
||||
);
|
||||
assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', 'Access Key has been set.');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Secret key'))
|
||||
.doesNotExist('Secret key is not shown because it does not get returned by the api.');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should not show identityTokenTtl or maxRetries if they have not been set', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
await enablePage.enable('aws', path);
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
// manually fill in attrs without using helper so we can exclude identityTokenTtl and maxRetries.
|
||||
await click(SES.aws.accessType('wif')); // toggle to wif
|
||||
await fillIn(GENERAL.inputByAttr('roleArn'), 'foo-role');
|
||||
await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'foo-audience');
|
||||
// manually fill in non-access type specific fields on root config so we can exclude Max Retries.
|
||||
await click(GENERAL.toggleGroup('Root config options'));
|
||||
await fillIn(GENERAL.inputByAttr('region'), 'eu-central-1');
|
||||
await click(GENERAL.saveButton);
|
||||
// the Serializer removes these two from the payload if the API returns their default value.
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Identity token TTL'))
|
||||
.doesNotExist('Identity token TTL does not show.');
|
||||
assert.dom(GENERAL.infoRowValue('Maximum retries')).doesNotExist('Maximum retries does not show.');
|
||||
// cleanup
|
||||
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('33s', `Default TTL has been set.`);
|
||||
assert.dom(GENERAL.infoRowValue('Max Lease TTL')).hasText('44s', `Max lease TTL has been set.`);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it shows AWS mount configuration details', async function (assert) {
|
||||
assert.expect(12);
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
this.server.get(`${path}/config/root`, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.ok(true, 'request made to config/root when navigating to the configuration page.');
|
||||
return { data: { id: path, type, attributes: payload } };
|
||||
});
|
||||
await enablePage.enable(type, path);
|
||||
createConfig(this.store, path, type); // create the aws root config in the store
|
||||
await click(SES.configTab);
|
||||
for (const key of expectedConfigKeys(type)) {
|
||||
assert.dom(GENERAL.infoRowLabel(key)).exists(`${key} on the ${type} config details exists.`);
|
||||
const responseKeyAndValue = expectedValueOfConfigKeys(type, key);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue(key))
|
||||
.hasText(responseKeyAndValue, `value for ${key} on the ${type} config details exists.`);
|
||||
}
|
||||
// check mount configuration details are present and accurate.
|
||||
await click(SES.configurationToggle);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Path'))
|
||||
.hasText(`${path}/`, 'mount path is displayed in the configuration details');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should update AWS configuration details after editing', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
await enablePage.enable(type, path);
|
||||
// create accessKey with value foo and confirm it shows up in the details page.
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig('withAccess');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', 'Access key is foo');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Region'))
|
||||
.doesNotExist('Region has not been added therefor it does not show up on the details view.');
|
||||
// edit root config details and lease config details and confirm the configuration.index page is updated.
|
||||
await click(SES.configure);
|
||||
// edit root config details
|
||||
await fillIn(GENERAL.inputByAttr('accessKey'), 'not-foo');
|
||||
await click(GENERAL.toggleGroup('Root config options'));
|
||||
await fillIn(GENERAL.inputByAttr('region'), 'ap-southeast-2');
|
||||
// add lease config details
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(GENERAL.saveButton);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Access key'))
|
||||
.hasText('not-foo', 'Access key has been updated to not-foo');
|
||||
assert.dom(GENERAL.infoRowValue('Region')).hasText('ap-southeast-2', 'Region has been added');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Default Lease TTL'))
|
||||
.hasText('33s', 'Default Lease TTL has been added');
|
||||
assert.dom(GENERAL.infoRowValue('Max Lease TTL')).hasText('44s', 'Max Lease TTL has been added');
|
||||
// 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');
|
||||
});
|
||||
|
||||
test('it should not make a post request if lease or root data was unchanged', async function (assert) {
|
||||
assert.expect(3);
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
await enablePage.enable(type, path);
|
||||
|
||||
this.server.post(configUrl(type, path), () => {
|
||||
assert.false(
|
||||
true,
|
||||
'post request was made to config/root when no data was changed. test should fail.'
|
||||
);
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', path), () => {
|
||||
assert.false(
|
||||
true,
|
||||
'post request was made to config/lease when no data was changed. test should fail.'
|
||||
);
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await click(GENERAL.saveButton);
|
||||
assert.true(
|
||||
this.flashInfoSpy.calledWith('No changes detected.'),
|
||||
'Flash message shows no changes detected.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/configuration`,
|
||||
'navigates back to the configuration index view'
|
||||
);
|
||||
assert.dom(GENERAL.emptyStateTitle).hasText('AWS not configured');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should reset models after saving', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
await enablePage.enable(type, path);
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig('withAccess');
|
||||
// the way to tell if a record has been unloaded is if the private key is not saved in the store (the API does not return it, but if the record was not unloaded it would have stayed.)
|
||||
await click(GENERAL.saveButton); // save the configuration
|
||||
await click(SES.configure);
|
||||
const privateKeyExists = this.store.peekRecord('aws/root-config', path).privateKey ? true : false;
|
||||
assert.false(
|
||||
privateKeyExists,
|
||||
'private key is not on the store record, meaning it was unloaded after save. This new record without the key comes from the API.'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.enableField('secretKey'))
|
||||
.exists('secret key field is wrapped inside an enableInput component');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('it should reset models after saving', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
await enablePage.enable(type, path);
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAwsConfig(true);
|
||||
// the way to tell if a record has been unloaded is if the private key is not saved in the store (the API does not return it, but if the record was not unloaded it would have stayed.)
|
||||
await click(SES.aws.save); // save the configuration
|
||||
await click(SES.configure);
|
||||
const privateKeyExists = this.store.peekRecord('aws/root-config', path).privateKey ? true : false;
|
||||
assert.false(
|
||||
privateKeyExists,
|
||||
'private key is not on the store record, meaning it was unloaded after save. This new record without the key comes from the API.'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.enableField('secretKey'))
|
||||
.exists('secret key field is wrapped inside an enableInput component');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
module('isCommunity', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'community';
|
||||
});
|
||||
|
||||
test('it does not show access type option and iam fields are shown', async function (assert) {
|
||||
const path = `aws-${this.uid}`;
|
||||
const type = 'aws';
|
||||
await enablePage.enable(type, path);
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
assert
|
||||
.dom(SES.aws.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-root-create')) {
|
||||
if (key === 'secretKey') {
|
||||
assert.dom(GENERAL.maskedInput(key)).exists(`${key} shows for root section.`);
|
||||
} else {
|
||||
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.`);
|
||||
}
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,10 +3,21 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { currentRouteName, currentURL, settled } from '@ember/test-helpers';
|
||||
import {
|
||||
currentRouteName,
|
||||
currentURL,
|
||||
settled,
|
||||
click,
|
||||
findAll,
|
||||
fillIn,
|
||||
visit,
|
||||
typeIn,
|
||||
} from '@ember/test-helpers';
|
||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
|
||||
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import page from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
@@ -17,6 +28,11 @@ import logout from 'vault/tests/pages/logout';
|
||||
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
import { mountableEngines } from 'vault/helpers/mountable-secret-engines'; // allEngines() includes enterprise engines, those are tested elsewhere
|
||||
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import { SELECTORS as OIDC } from 'vault/tests/helpers/oidc-config';
|
||||
import { adminOidcCreateRead, adminOidcCreate } from 'vault/tests/helpers/secret-engine/policy-generator';
|
||||
import { WIF_ENGINES } from 'vault/helpers/mountable-secret-engines';
|
||||
|
||||
const consoleComponent = create(consoleClass);
|
||||
|
||||
@@ -298,4 +314,107 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
|
||||
`${v1} navigates to list route`
|
||||
);
|
||||
});
|
||||
|
||||
module('WIF secret engines', function () {
|
||||
test('it sets identity_token_key on mount config using search select list, resets after', async function (assert) {
|
||||
// create an oidc/key
|
||||
await runCmd(`write identity/oidc/key/some-key allowed_client_ids="*"`);
|
||||
|
||||
for (const engine of WIF_ENGINES) {
|
||||
await page.visit();
|
||||
await page.selectType(engine);
|
||||
await click(GENERAL.toggleGroup('Method Options'));
|
||||
assert
|
||||
.dom('[data-test-search-select-with-modal]')
|
||||
.exists('Search select with modal component renders');
|
||||
await clickTrigger('#key');
|
||||
const dropdownOptions = findAll('[data-option-index]').map((o) => o.innerText);
|
||||
assert.ok(dropdownOptions.includes('some-key'), 'search select options show some-key');
|
||||
await click(GENERAL.searchSelect.option(GENERAL.searchSelect.optionIndex('some-key')));
|
||||
assert
|
||||
.dom(GENERAL.searchSelect.selectedOption())
|
||||
.hasText('some-key', 'some-key was selected and displays in the search select');
|
||||
}
|
||||
// Go back and choose a non-wif engine type
|
||||
await page.back();
|
||||
await page.selectType('ssh');
|
||||
assert
|
||||
.dom('[data-test-search-select-with-modal]')
|
||||
.doesNotExist('for type ssh, the modal field does not render.');
|
||||
// cleanup
|
||||
await runCmd(`delete identity/oidc/key/some-key`);
|
||||
});
|
||||
|
||||
test('it allows a user with permissions to oidc/key to create an identity_token_key', async function (assert) {
|
||||
for (const engine of WIF_ENGINES) {
|
||||
const path = `secrets-adminPolicy-${engine}`;
|
||||
const newKey = `key-${uuidv4()}`;
|
||||
const secrets_admin_policy = adminOidcCreateRead(path);
|
||||
const secretsAdminToken = await runCmd(
|
||||
tokenWithPolicyCmd(`secrets-admin-${path}`, secrets_admin_policy)
|
||||
);
|
||||
|
||||
await logout.visit();
|
||||
await authPage.login(secretsAdminToken);
|
||||
await page.visit();
|
||||
await page.selectType(engine);
|
||||
await page.path(path);
|
||||
await click(GENERAL.toggleGroup('Method Options'));
|
||||
await clickTrigger('#key');
|
||||
// create new key
|
||||
await fillIn(GENERAL.searchSelect.searchInput, newKey);
|
||||
await click(GENERAL.searchSelect.options);
|
||||
assert.dom('#search-select-modal').exists('modal with form opens');
|
||||
assert.dom('[data-test-modal-title]').hasText('Create new key', 'Create key modal renders');
|
||||
|
||||
await click(OIDC.keySaveButton);
|
||||
assert.dom('#search-select-modal').doesNotExist('modal disappears onSave');
|
||||
assert.dom(GENERAL.searchSelect.selectedOption()).hasText(newKey, `${newKey} is now selected`);
|
||||
|
||||
await page.submit();
|
||||
await visit(`/vault/secrets/${path}/configuration`);
|
||||
await click(SES.configurationToggle);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Identity Token Key'))
|
||||
.hasText(newKey, 'shows identity token key on configuration page');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
await runCmd(`delete identity/oidc/key/some-key`);
|
||||
await runCmd(`delete identity/oidc/key/${newKey}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('it allows user with NO access to oidc/key to manually input an identity_token_key', async function (assert) {
|
||||
for (const engine of WIF_ENGINES) {
|
||||
const path = `secrets-noOidcAdmin-${engine}`;
|
||||
const secretsNoOidcAdminPolicy = adminOidcCreate(path);
|
||||
const secretsNoOidcAdminToken = await runCmd(
|
||||
tokenWithPolicyCmd(`secrets-noOidcAdmin-${path}`, secretsNoOidcAdminPolicy)
|
||||
);
|
||||
// create an oidc/key that they can then use even if they can't read it.
|
||||
await runCmd(`write identity/oidc/key/general-key allowed_client_ids="*"`);
|
||||
|
||||
await logout.visit();
|
||||
await authPage.login(secretsNoOidcAdminToken);
|
||||
await page.visit();
|
||||
await page.selectType(engine);
|
||||
await page.path(path);
|
||||
await click(GENERAL.toggleGroup('Method Options'));
|
||||
// type-in fallback component to create new key
|
||||
await typeIn(GENERAL.inputSearch('key'), 'general-key');
|
||||
await page.submit();
|
||||
assert
|
||||
.dom(GENERAL.latestFlashContent)
|
||||
.hasText(`Successfully mounted the aws secrets engine at ${path}.`);
|
||||
|
||||
await visit(`/vault/secrets/${path}/configuration`);
|
||||
await click(SES.configurationToggle);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Identity Token Key'))
|
||||
.hasText('general-key', 'shows identity token key on configuration page');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ export const GENERAL = {
|
||||
|
||||
filter: (name: string) => `[data-test-filter="${name}"]`,
|
||||
filterInput: '[data-test-filter-input]',
|
||||
inputSearch: (attr: string) => `[data-test-input-search="${attr}"]`,
|
||||
filterInputExplicit: '[data-test-filter-input-explicit]',
|
||||
filterInputExplicitSearch: '[data-test-filter-input-explicit-search]',
|
||||
confirmModalInput: '[data-test-confirmation-modal-input]',
|
||||
@@ -72,6 +73,7 @@ export const GENERAL = {
|
||||
selectedOption: (index = 0) => `[data-test-selected-option="${index}"]`,
|
||||
noMatch: '.ember-power-select-option--no-matches-message',
|
||||
removeSelected: '[data-test-selected-list-button="delete"]',
|
||||
searchInput: '.ember-power-select-search-input',
|
||||
},
|
||||
overviewCard: {
|
||||
container: (title: string) => `[data-test-overview-card-container="${title}"]`,
|
||||
|
||||
36
ui/tests/helpers/secret-engine/policy-generator.ts
Normal file
36
ui/tests/helpers/secret-engine/policy-generator.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// This is policy can mount a secret engine
|
||||
// and list and create oidc keys, relevant for setting identity_key_token for WIF
|
||||
export const adminOidcCreateRead = (mountPath: string) => {
|
||||
return `
|
||||
path "sys/mounts/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list"]
|
||||
},
|
||||
path "identity/oidc/key/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list"]
|
||||
},
|
||||
path "${mountPath}/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list"]
|
||||
},
|
||||
`;
|
||||
};
|
||||
|
||||
// This policy can mount the engine
|
||||
// But does not have access to oidc/key list or read
|
||||
export const adminOidcCreate = (mountPath: string) => {
|
||||
return `
|
||||
path "sys/mounts/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list"]
|
||||
},
|
||||
path "${mountPath}/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list"]
|
||||
},
|
||||
path "identity/oidc/key/*" {
|
||||
capabilities = ["create", "update"]
|
||||
},
|
||||
`;
|
||||
};
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import { click, fillIn } from '@ember/test-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 { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const createSecretsEngine = (store, type, path) => {
|
||||
store.pushPayload('secret-engine', {
|
||||
@@ -19,19 +21,56 @@ export const createSecretsEngine = (store, type, path) => {
|
||||
return store.peekRecord('secret-engine', path);
|
||||
};
|
||||
|
||||
const createAwsRootConfig = (store, backend) => {
|
||||
store.pushPayload('aws/root-config', {
|
||||
id: backend,
|
||||
modelName: 'aws/root-config',
|
||||
const createAwsRootConfig = (store, backend, accessType = 'iam') => {
|
||||
// clear any records first
|
||||
store.unloadAll('aws/root-config');
|
||||
if (accessType === 'wif') {
|
||||
store.pushPayload('aws/root-config', {
|
||||
id: backend,
|
||||
modelName: 'aws/root-config',
|
||||
data: {
|
||||
backend,
|
||||
role_arn: '123-role',
|
||||
identity_token_audience: '123-audience',
|
||||
identity_token_ttl: 7200,
|
||||
},
|
||||
});
|
||||
} else if (accessType === 'no-access') {
|
||||
// set root config options that are not associated with accessType 'wif' or 'iam'
|
||||
store.pushPayload('aws/root-config', {
|
||||
id: backend,
|
||||
modelName: 'aws/root-config',
|
||||
data: {
|
||||
backend,
|
||||
region: 'ap-northeast-1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
store.pushPayload('aws/root-config', {
|
||||
id: backend,
|
||||
modelName: 'aws/root-config',
|
||||
data: {
|
||||
backend,
|
||||
region: 'us-west-2',
|
||||
access_key: '123-key',
|
||||
iam_endpoint: 'iam-endpoint',
|
||||
sts_endpoint: 'sts-endpoint',
|
||||
max_retries: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
return store.peekRecord('aws/root-config', backend);
|
||||
};
|
||||
|
||||
const createIssuerConfig = (store) => {
|
||||
store.pushPayload('identity/oidc/config', {
|
||||
id: 'identity-oidc-config',
|
||||
modelName: 'identity/oidc/config',
|
||||
data: {
|
||||
backend,
|
||||
region: 'us-west-2',
|
||||
access_key: '123-key',
|
||||
iam_endpoint: 'iam-endpoint',
|
||||
sts_endpoint: 'sts-endpoint',
|
||||
issuer: ``,
|
||||
},
|
||||
});
|
||||
return store.peekRecord('aws/root-config', backend);
|
||||
return store.peekRecord('identity/oidc/config', 'identity-oidc-config');
|
||||
};
|
||||
|
||||
const createAwsLeaseConfig = (store, backend) => {
|
||||
@@ -77,6 +116,12 @@ export const createConfig = (store, backend, type) => {
|
||||
switch (type) {
|
||||
case 'aws':
|
||||
return createAwsRootConfig(store, backend);
|
||||
case 'aws-wif':
|
||||
return createAwsRootConfig(store, backend, 'wif');
|
||||
case 'aws-no-access':
|
||||
return createAwsRootConfig(store, backend, 'no-access');
|
||||
case 'issuer':
|
||||
return createIssuerConfig(store, backend);
|
||||
case 'aws-lease':
|
||||
return createAwsLeaseConfig(store, backend);
|
||||
case 'ssh':
|
||||
@@ -92,6 +137,10 @@ export const expectedConfigKeys = (type) => {
|
||||
return ['Default Lease TTL', 'Max Lease TTL'];
|
||||
case 'aws-root-create':
|
||||
return ['accessKey', 'secretKey', 'region', 'iamEndpoint', 'stsEndpoint', 'maxRetries'];
|
||||
case 'aws-root-create-wif':
|
||||
return ['issuer', 'roleArn', 'identityTokenAudience', 'Identity token TTL'];
|
||||
case 'aws-root-create-iam':
|
||||
return ['accessKey', 'secretKey'];
|
||||
case 'ssh':
|
||||
return ['Public key', 'Generate signing key'];
|
||||
}
|
||||
@@ -108,7 +157,7 @@ const valueOfAwsKeys = (string) => {
|
||||
case 'STS endpoint':
|
||||
return 'sts-endpoint';
|
||||
case 'Maximum retries':
|
||||
return '-1';
|
||||
return '1';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -130,22 +179,30 @@ export const expectedValueOfConfigKeys = (type, string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const fillInAwsConfig = async (withAccess = true, withAccessOptions = false, withLease = false) => {
|
||||
if (withAccess) {
|
||||
export const fillInAwsConfig = async (situation = 'withAccess') => {
|
||||
if (situation === 'withAccess') {
|
||||
await fillIn(GENERAL.inputByAttr('accessKey'), 'foo');
|
||||
await fillIn(GENERAL.maskedInput('secretKey'), 'bar');
|
||||
}
|
||||
if (withAccessOptions) {
|
||||
if (situation === 'withAccessOptions') {
|
||||
await click(GENERAL.toggleGroup('Root config options'));
|
||||
await fillIn(GENERAL.inputByAttr('region'), 'ca-central-1');
|
||||
await fillIn(GENERAL.inputByAttr('iamEndpoint'), 'iam-endpoint');
|
||||
await fillIn(GENERAL.inputByAttr('stsEndpoint'), 'sts-endpoint');
|
||||
await fillIn(GENERAL.inputByAttr('maxRetries'), '3');
|
||||
}
|
||||
if (withLease) {
|
||||
if (situation === 'withLease') {
|
||||
await click(GENERAL.ttl.toggle('Default Lease TTL'));
|
||||
await fillIn(GENERAL.ttl.input('Default Lease TTL'), '33');
|
||||
await click(GENERAL.ttl.toggle('Max Lease TTL'));
|
||||
await fillIn(GENERAL.ttl.input('Max Lease TTL'), '44');
|
||||
}
|
||||
if (situation === 'withWif') {
|
||||
await click(SES.aws.accessType('wif')); // toggle to wif
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), `http://bar.${uuidv4()}`); // make random because global setting
|
||||
await fillIn(GENERAL.inputByAttr('roleArn'), 'foo-role');
|
||||
await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'foo-audience');
|
||||
await click(GENERAL.ttl.toggle('Identity token TTL'));
|
||||
await fillIn(GENERAL.ttl.input('Identity token TTL'), '7200');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,9 +25,14 @@ export const SECRET_ENGINE_SELECTORS = {
|
||||
rootForm: '[data-test-root-form]',
|
||||
accessTitle: '[data-test-access-title]',
|
||||
leaseTitle: '[data-test-lease-title]',
|
||||
save: '[data-test-save]',
|
||||
cancel: '[data-test-cancel]',
|
||||
deleteRole: (role: string) => `[data-test-aws-role-delete="${role}"]`,
|
||||
accessTypeSection: '[data-test-access-type-section]',
|
||||
accessTypeSubtext: '[data-test-access-type-subtext]',
|
||||
accessType: (type: string) => `[data-test-access-type="${type}"]`,
|
||||
issuerWarningModal: '[data-test-issuer-warning]',
|
||||
issuerWarningMessage: '[data-test-issuer-warning-message]',
|
||||
issuerWarningSave: '[data-test-issuer-save]',
|
||||
issuerWarningCancel: '[data-test-issuer-cancel]',
|
||||
},
|
||||
ssh: {
|
||||
configureForm: '[data-test-configure-form]',
|
||||
|
||||
@@ -250,10 +250,14 @@ module('Integration | Component | form field', function (hooks) {
|
||||
assert.strictEqual(component.fields.objectAt(0).labelValue, 'Not Foo', 'renders the label from options');
|
||||
});
|
||||
|
||||
test('it renders a help tooltip', async function (assert) {
|
||||
await setup.call(this, createAttr('foo', 'string', { helpText: 'Here is some help text' }));
|
||||
test('it renders a help tooltip and placeholder', async function (assert) {
|
||||
await setup.call(
|
||||
this,
|
||||
createAttr('foo', 'string', { helpText: 'Here is some help text', placeholder: 'example::value' })
|
||||
);
|
||||
await component.tooltipTrigger();
|
||||
assert.ok(component.hasTooltip, 'renders the tooltip component');
|
||||
assert.dom('[data-test-input="foo"]').hasAttribute('placeholder', 'example::value');
|
||||
});
|
||||
|
||||
test('it should not expand and toggle ttl when default 0s value is present', async function (assert) {
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
import { later, _cancelTimers as cancelTimers } from '@ember/runloop';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, settled } from '@ember/test-helpers';
|
||||
import { render, settled, click, typeIn } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { allowAllCapabilitiesStub, noopStub } from 'vault/tests/helpers/stubs';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
import { create } from 'ember-cli-page-object';
|
||||
@@ -190,5 +191,46 @@ module('Integration | Component | mount backend form', function (hooks) {
|
||||
'Renders correct flash message'
|
||||
);
|
||||
});
|
||||
|
||||
module('WIF secret engines', function () {
|
||||
test('it shows identityTokenKey when type is aws and hides when its not', async function (assert) {
|
||||
await render(
|
||||
hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
);
|
||||
await component.selectType('ldap');
|
||||
|
||||
await click(GENERAL.toggleGroup('Method Options'));
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('identityTokenKey'))
|
||||
.doesNotExist(`Identity token key field hidden when type=${this.model.type}`);
|
||||
|
||||
await component.back();
|
||||
await component.selectType('aws');
|
||||
await click(GENERAL.toggleGroup('Method Options'));
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('identityTokenKey'))
|
||||
.exists(`Identity token key field shows when type=${this.model.type}`);
|
||||
});
|
||||
|
||||
test('it updates identityTokeKey if user has changed it', async function (assert) {
|
||||
await render(
|
||||
hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
);
|
||||
await component.selectType('aws');
|
||||
assert.strictEqual(
|
||||
this.model.config.identityTokenKey,
|
||||
undefined,
|
||||
'On init identityTokenKey is not set on the model'
|
||||
);
|
||||
|
||||
await click(GENERAL.toggleGroup('Method Options'));
|
||||
await typeIn(GENERAL.inputSearch('key'), 'default');
|
||||
assert.strictEqual(
|
||||
this.model.config.identityTokenKey,
|
||||
'default',
|
||||
'updates model with default identityTokenKey'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import hbs from 'htmlbars-inline-precompile';
|
||||
import ss from 'vault/tests/pages/components/search-select';
|
||||
import sinon from 'sinon';
|
||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
const component = create(ss);
|
||||
|
||||
@@ -245,24 +246,35 @@ module('Integration | Component | search select with modal', function (hooks) {
|
||||
assert.ok(this.onChange.notCalled, 'onChange is not called');
|
||||
});
|
||||
|
||||
test('it renders fallback component if both models return 403', async function (assert) {
|
||||
assert.expect(7);
|
||||
this.server.get('sys/policies/acl', () => {
|
||||
return new Response(
|
||||
403,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({ errors: ['permission denied'] })
|
||||
);
|
||||
});
|
||||
this.server.get('sys/policies/rgp', () => {
|
||||
return new Response(
|
||||
403,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({ errors: ['permission denied'] })
|
||||
);
|
||||
module('fallback component', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.server.get('sys/policies/acl', () => {
|
||||
return new Response(
|
||||
403,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({ errors: ['permission denied'] })
|
||||
);
|
||||
});
|
||||
this.server.get('sys/policies/rgp', () => {
|
||||
return new Response(
|
||||
403,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({ errors: ['permission denied'] })
|
||||
);
|
||||
});
|
||||
this.server.get('identity/oidc/key?list=true', () => {
|
||||
return new Response(
|
||||
403,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({ errors: ['permission denied'] })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
test('it renders fallback component if both models return 403', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
await render(hbs`
|
||||
<SearchSelectWithModal
|
||||
@id="policies"
|
||||
@label="Policies"
|
||||
@@ -274,23 +286,71 @@ module('Integration | Component | search select with modal', function (hooks) {
|
||||
@modalFormTemplate="modal-form/policy-template"
|
||||
/>
|
||||
`);
|
||||
assert.dom('[data-test-component="string-list"]').exists('renders fallback component');
|
||||
assert.false(component.hasTrigger, 'does not render power select trigger');
|
||||
await fillIn('[data-test-string-list-input="0"]', 'string-list-policy');
|
||||
await click('[data-test-string-list-button="add"]');
|
||||
assert
|
||||
.dom('[data-test-string-list-input="0"]')
|
||||
.hasValue('string-list-policy', 'first row renders inputted string');
|
||||
assert
|
||||
.dom('[data-test-string-list-row="0"] [data-test-string-list-button="delete"]')
|
||||
.exists('first row renders delete icon');
|
||||
assert.dom('[data-test-string-list-row="1"]').exists('renders second input row');
|
||||
assert
|
||||
.dom('[data-test-string-list-row="1"] [data-test-string-list-button="add"]')
|
||||
.exists('second row renders add icon');
|
||||
assert.ok(
|
||||
this.onChange.calledWithExactly(['string-list-policy']),
|
||||
'onChange is called only after item is created'
|
||||
);
|
||||
assert.dom('[data-test-component="string-list"]').exists('renders fallback component');
|
||||
assert.false(component.hasTrigger, 'does not render power select trigger');
|
||||
await fillIn('[data-test-string-list-input="0"]', 'string-list-policy');
|
||||
await click('[data-test-string-list-button="add"]');
|
||||
assert
|
||||
.dom('[data-test-string-list-input="0"]')
|
||||
.hasValue('string-list-policy', 'first row renders inputted string');
|
||||
assert
|
||||
.dom('[data-test-string-list-row="0"] [data-test-string-list-button="delete"]')
|
||||
.exists('first row renders delete icon');
|
||||
assert.dom('[data-test-string-list-row="1"]').exists('renders second input row');
|
||||
assert
|
||||
.dom('[data-test-string-list-row="1"] [data-test-string-list-button="add"]')
|
||||
.exists('second row renders add icon');
|
||||
assert.ok(
|
||||
this.onChange.calledWithExactly(['string-list-policy']),
|
||||
'onChange is called only after item is created'
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders fallback placeholder text for fallback component', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
await render(hbs`
|
||||
<SearchSelectWithModal
|
||||
@id="key"
|
||||
@label="Keys"
|
||||
@models={{array "oidc/key"}}
|
||||
@inputValue={{this.policies}}
|
||||
@onChange={{this.onChange}}
|
||||
@fallbackComponent="input-search"
|
||||
@modalFormTemplate="modal-form/oidc-key-template"
|
||||
@selectLimit="1"
|
||||
@placeholder="Search or type to create a key item"
|
||||
@fallbackComponentPlaceholder="Input key name"
|
||||
/>
|
||||
`);
|
||||
assert
|
||||
.dom(GENERAL.inputSearch('key'))
|
||||
.hasAttribute('placeholder', 'Input key name', 'Fallback placeholder was passed to input search');
|
||||
});
|
||||
|
||||
test('it renders placeholder text for fallback component', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
await render(hbs`
|
||||
<SearchSelectWithModal
|
||||
@id="key"
|
||||
@label="Keys"
|
||||
@models={{array "oidc/key"}}
|
||||
@inputValue={{this.policies}}
|
||||
@onChange={{this.onChange}}
|
||||
@fallbackComponent="input-search"
|
||||
@modalFormTemplate="modal-form/oidc-key-template"
|
||||
@selectLimit="1"
|
||||
@placeholder="Search or type to create a key item"
|
||||
/>
|
||||
`);
|
||||
assert
|
||||
.dom(GENERAL.inputSearch('key'))
|
||||
.hasAttribute(
|
||||
'placeholder',
|
||||
'Search or type to create a key item',
|
||||
'Placeholder was passed to input search'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
expectedValueOfConfigKeys,
|
||||
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
|
||||
module('Integration | Component | SecretEngine/configuration-details', function (hooks) {
|
||||
module('Integration | Component | SecretEngine/ConfigurationDetails', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
|
||||
@@ -19,8 +19,9 @@ import {
|
||||
configUrl,
|
||||
fillInAwsConfig,
|
||||
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
import { capabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
|
||||
module('Integration | Component | SecretEngine/configure-aws', function (hooks) {
|
||||
module('Integration | Component | SecretEngine/ConfigureAws', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
@@ -34,126 +35,439 @@ module('Integration | Component | SecretEngine/configure-aws', function (hooks)
|
||||
|
||||
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}} @backendPath={{this.id}} />
|
||||
<SecretEngine::ConfigureAws @rootConfig={{this.rootConfig}} @leaseConfig={{this.leaseConfig}} @issuerConfig={{this.issuerConfig}} @backendPath={{this.id}} />
|
||||
`);
|
||||
};
|
||||
});
|
||||
module('Create view', function () {
|
||||
test('it renders fields', async function (assert) {
|
||||
assert.expect(11);
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.aws.rootForm).exists('it lands on the aws root configuration form.');
|
||||
assert.dom(SES.aws.accessTitle).exists('Access section is rendered');
|
||||
assert.dom(SES.aws.leaseTitle).exists('Lease section is rendered');
|
||||
// check all the form fields are present
|
||||
await click(GENERAL.toggleGroup('Root config options'));
|
||||
for (const key of expectedConfigKeys('aws-root-create')) {
|
||||
if (key === 'secretKey') {
|
||||
assert.dom(GENERAL.maskedInput(key)).exists(`${key} shows for root section.`);
|
||||
} else {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section.`);
|
||||
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.aws.accessTitle).exists('Access section is rendered');
|
||||
assert.dom(SES.aws.leaseTitle).exists('Lease section is rendered');
|
||||
assert.dom(SES.aws.accessTypeSection).exists('Access type section is rendered');
|
||||
assert.dom(SES.aws.accessType('iam')).isChecked('defaults to showing IAM access type checked');
|
||||
assert.dom(SES.aws.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-root-create')) {
|
||||
if (key === 'secretKey') {
|
||||
assert.dom(GENERAL.maskedInput(key)).exists(`${key} shows for root section.`);
|
||||
} else {
|
||||
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.`);
|
||||
}
|
||||
});
|
||||
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 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.'
|
||||
);
|
||||
test('it renders wif fields when selected', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await click(SES.aws.accessType('wif'));
|
||||
// check for the wif fields only
|
||||
for (const key of expectedConfigKeys('aws-root-create-wif')) {
|
||||
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-root-create-iam')) {
|
||||
if (key === 'secretKey') {
|
||||
assert.dom(GENERAL.maskedInput(key)).doesNotExist(`${key} does not show when wif is selected.`);
|
||||
} else {
|
||||
assert.dom(GENERAL.inputByAttr(key)).doesNotExist(`${key} does not show when wif is selected.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
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(SES.aws.save);
|
||||
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(true, false, true);
|
||||
await click(SES.aws.save);
|
||||
assert.dom(GENERAL.messageError).exists('API error surfaced to user');
|
||||
assert.dom(GENERAL.inlineError).exists('User shown inline error message');
|
||||
});
|
||||
test('it clears wif/iam inputs after toggling accessType', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await fillInAwsConfig('withAccess');
|
||||
await fillInAwsConfig('withLease');
|
||||
await click(SES.aws.accessType('wif')); // toggle to wif
|
||||
await fillInAwsConfig('withWif');
|
||||
await click(SES.aws.accessType('iam')); // toggle to wif
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('accessKey'))
|
||||
.hasValue('', 'accessKey is cleared after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.maskedInput('secretKey'))
|
||||
.hasValue('', 'secretKey is cleared after toggling accessType');
|
||||
|
||||
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.');
|
||||
await click(SES.aws.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue('', 'issue shows no value after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasAttribute(
|
||||
'placeholder',
|
||||
'https://vault.prod/v1/identity/oidc',
|
||||
'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');
|
||||
});
|
||||
this.server.post(configUrl('aws-lease', this.id), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
|
||||
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.aws.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.aws.accessType('iam'));
|
||||
await click(SES.aws.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(
|
||||
this.issuerConfig.issuer,
|
||||
'issuer value is still the same global value after toggling accessType'
|
||||
);
|
||||
});
|
||||
// fill in both lease and root endpoints to ensure that both payloads are attempted to be sent
|
||||
await fillInAwsConfig(true, false, true);
|
||||
await click(SES.aws.save);
|
||||
|
||||
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 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);
|
||||
|
||||
test('it transitions without sending a lease or root 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.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.'
|
||||
);
|
||||
});
|
||||
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.'
|
||||
);
|
||||
});
|
||||
// fill in both lease and root endpoints to ensure that both payloads are attempted to be sent
|
||||
await fillInAwsConfig(true, false, true);
|
||||
await click(SES.aws.cancel);
|
||||
|
||||
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.'
|
||||
);
|
||||
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.aws.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.aws.accessType('wif'));
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://change.me.no.read');
|
||||
await click(GENERAL.saveButton);
|
||||
assert
|
||||
.dom(SES.aws.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.aws.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasAttribute('placeholder', 'https://vault.prod/v1/identity/oidc', '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.aws.issuerWarningModal).exists('issuer modal exists');
|
||||
assert
|
||||
.dom(SES.aws.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.aws.issuerWarningCancel);
|
||||
assert.dom(SES.aws.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.aws.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.aws.issuerWarningModal).exists('issue warning modal exists');
|
||||
await click(SES.aws.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.aws.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.aws.issuerWarningModal).exists('issuer warning modal exists');
|
||||
|
||||
await click(SES.aws.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.aws.accessTitle).exists('Access section is rendered');
|
||||
assert.dom(SES.aws.leaseTitle).exists('Lease section is rendered');
|
||||
assert
|
||||
.dom(SES.aws.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-root-create')) {
|
||||
if (key === 'secretKey') {
|
||||
assert.dom(GENERAL.maskedInput(key)).exists(`${key} shows for root section.`);
|
||||
} else {
|
||||
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.aws.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) {
|
||||
@@ -161,40 +475,116 @@ module('Integration | Component | SecretEngine/configure-aws', function (hooks)
|
||||
this.rootConfig = createConfig(this.store, this.id, 'aws');
|
||||
this.leaseConfig = createConfig(this.store, this.id, 'aws-lease');
|
||||
});
|
||||
|
||||
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.'
|
||||
);
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
await click(GENERAL.enableField('secretKey'));
|
||||
await click('[data-test-button="toggle-masked"]');
|
||||
await fillIn(GENERAL.maskedInput('secretKey'), 'new-secret');
|
||||
await click(SES.aws.save);
|
||||
test('it defaults to IAM accessType if IAM fields are already set', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.aws.accessType('iam')).isChecked('IAM accessType is checked');
|
||||
assert.dom(SES.aws.accessType('iam')).isDisabled('IAM accessType is disabled');
|
||||
assert.dom(SES.aws.accessType('wif')).isNotChecked('WIF accessType is not checked');
|
||||
assert.dom(SES.aws.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert
|
||||
.dom(SES.aws.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.aws.accessType('wif')).isChecked('WIF accessType is checked');
|
||||
assert.dom(SES.aws.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert.dom(SES.aws.accessType('iam')).isNotChecked('IAM accessType is not checked');
|
||||
assert.dom(SES.aws.accessType('iam')).isDisabled('IAM accessType is disabled');
|
||||
assert.dom(GENERAL.inputByAttr('roleArn')).hasValue(this.rootConfig.roleArn);
|
||||
assert
|
||||
.dom(SES.aws.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.aws.accessType('wif')).isChecked('WIF accessType is checked');
|
||||
assert.dom(SES.aws.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.aws.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled');
|
||||
assert.dom(SES.aws.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.maskedInput('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.aws.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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,146 +4,138 @@
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render, click, fillIn } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import { createConfig } from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Integration | Component | SecretEngine/configure-ssh', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
module('Unit | Adapter | secret engine', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
const router = this.owner.lookup('service:router');
|
||||
this.id = 'ssh-test';
|
||||
this.model = this.store.createRecord('ssh/ca-config', { backend: this.id });
|
||||
this.transitionStub = sinon.stub(router, 'transitionTo');
|
||||
this.refreshStub = sinon.stub(router, 'refresh');
|
||||
});
|
||||
|
||||
test('it shows create fields if not configured', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
assert.dom(GENERAL.maskedInput('privateKey')).hasNoText('Private key is empty and reset');
|
||||
assert.dom(GENERAL.inputByAttr('publicKey')).hasNoText('Public key is empty and reset');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('generateSigningKey'))
|
||||
.isChecked('Generate signing key is checked by default');
|
||||
});
|
||||
|
||||
test('it should go back to parent route on cancel', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
|
||||
await click(SES.ssh.cancel);
|
||||
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', 'ssh-test'),
|
||||
'On cancel the router transitions to the parent configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should validate form fields', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
await fillIn(GENERAL.inputByAttr('publicKey'), 'hello');
|
||||
await click(SES.ssh.save);
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText(
|
||||
'You must provide a Public and Private keys or leave both unset.',
|
||||
'Public key validation error renders.'
|
||||
);
|
||||
|
||||
await click(GENERAL.inputByAttr('generateSigningKey'));
|
||||
await click(SES.ssh.save);
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText(
|
||||
'You must provide a Public and Private keys or leave both unset.',
|
||||
'Generate signing key validation message shows.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should generate signing key', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.post('/ssh-test/config/ca', (schema, req) => {
|
||||
const data = JSON.parse(req.requestBody);
|
||||
const expected = {
|
||||
backend: this.id,
|
||||
generate_signing_key: true,
|
||||
const storeStub = {
|
||||
serializerFor() {
|
||||
return {
|
||||
serializeIntoHash() {},
|
||||
};
|
||||
assert.deepEqual(expected, data, 'POST request made to save ca-config with correct properties');
|
||||
});
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.model}}
|
||||
@id={{this.id}}
|
||||
/>
|
||||
`);
|
||||
},
|
||||
};
|
||||
const type = {
|
||||
modelName: 'secret-engine',
|
||||
};
|
||||
|
||||
await click(SES.ssh.save);
|
||||
assert.dom(SES.ssh.editConfigSection).exists('renders the edit configuration section of the form');
|
||||
test('Empty query', function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get('/sys/internal/ui/mounts', () => {
|
||||
assert.ok('query calls the correct url');
|
||||
return {};
|
||||
});
|
||||
const adapter = this.owner.lookup('adapter:secret-engine');
|
||||
adapter['query'](storeStub, type, {});
|
||||
});
|
||||
test('Query with a path', function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get('/sys/internal/ui/mounts/foo', () => {
|
||||
assert.ok('query calls the correct url');
|
||||
return {};
|
||||
});
|
||||
const adapter = this.owner.lookup('adapter:secret-engine');
|
||||
adapter['query'](storeStub, type, { path: 'foo' });
|
||||
});
|
||||
|
||||
module('editing', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.editId = 'ssh-edit-me';
|
||||
this.editModel = createConfig(this.store, 'ssh-edit-me', 'ssh');
|
||||
test('Query with nested path', function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get('/sys/internal/ui/mounts/foo/bar/baz', () => {
|
||||
assert.ok('query calls the correct url');
|
||||
return {};
|
||||
});
|
||||
test('it populates fields when editing', async function (assert) {
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.editModel}}
|
||||
@id={{this.editId}}
|
||||
/>
|
||||
`);
|
||||
assert
|
||||
.dom(SES.ssh.editConfigSection)
|
||||
.exists('renders the edit configuration section of the form and not the create part');
|
||||
assert.dom(GENERAL.inputByAttr('public-key')).hasText('***********', 'public key is masked');
|
||||
await click('[data-test-button="toggle-masked"]');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('public-key'))
|
||||
.hasText(this.editModel.publicKey, 'public key is unmasked and shows the actual value');
|
||||
const adapter = this.owner.lookup('adapter:secret-engine');
|
||||
adapter['query'](storeStub, type, { path: 'foo/bar/baz' });
|
||||
});
|
||||
|
||||
module('WIF secret engines', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
});
|
||||
|
||||
test('it allows you to delete a public key', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.delete('/ssh-edit-me/config/ca', () => {
|
||||
assert.true(true, 'DELETE request made to ca-config with correct properties');
|
||||
test('it should make request to correct endpoint when creating new record', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/sys/mounts/aws-wif', (schema, req) => {
|
||||
assert.deepEqual(
|
||||
JSON.parse(req.requestBody),
|
||||
{
|
||||
path: 'aws-wif',
|
||||
type: 'aws',
|
||||
config: { id: 'aws-wif', identity_token_key: 'test-key', listing_visibility: 'hidden' },
|
||||
},
|
||||
'Correct payload is sent when adding aws secret engine with identity_token_key set'
|
||||
);
|
||||
return {};
|
||||
});
|
||||
await render(hbs`
|
||||
<SecretEngine::ConfigureSsh
|
||||
@model={{this.editModel}}
|
||||
@id={{this.editId}}
|
||||
/>
|
||||
`);
|
||||
// delete Public key
|
||||
await click(SES.ssh.delete);
|
||||
assert.dom(GENERAL.confirmMessage).hasText('Confirming will remove the CA certificate information.');
|
||||
await click(GENERAL.confirmButton);
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration.edit', 'ssh-edit-me'),
|
||||
'On delete the router transitions to the current route.'
|
||||
);
|
||||
const mountData = {
|
||||
id: 'aws-wif',
|
||||
path: 'aws-wif',
|
||||
type: 'aws',
|
||||
config: this.store.createRecord('mount-config', {
|
||||
identityTokenKey: 'test-key',
|
||||
}),
|
||||
uuid: 'f1739f9d-dfc0-83c8-011f-ec17103a06c2',
|
||||
};
|
||||
const record = this.store.createRecord('secret-engine', mountData);
|
||||
await record.save();
|
||||
});
|
||||
|
||||
test('it should not send identity_token_key if not set', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/sys/mounts/aws-wif', (schema, req) => {
|
||||
assert.deepEqual(
|
||||
JSON.parse(req.requestBody),
|
||||
{
|
||||
path: 'aws-wif',
|
||||
type: 'aws',
|
||||
config: { id: 'aws-wif', max_lease_ttl: '125h', listing_visibility: 'hidden' },
|
||||
},
|
||||
'Correct payload is sent when adding aws secret engine with no identity_token_key set'
|
||||
);
|
||||
return {};
|
||||
});
|
||||
const mountData = {
|
||||
id: 'aws-wif',
|
||||
path: 'aws-wif',
|
||||
type: 'aws',
|
||||
config: this.store.createRecord('mount-config', {
|
||||
maxLeaseTtl: '125h',
|
||||
}),
|
||||
uuid: 'f1739f9d-dfc0-83c8-011f-ec17103a06c2',
|
||||
};
|
||||
const record = this.store.createRecord('secret-engine', mountData);
|
||||
await record.save();
|
||||
});
|
||||
|
||||
test('it should not send identity_token_key if set on a non-WIF secret engine', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/sys/mounts/cubbyhole-test', (schema, req) => {
|
||||
assert.deepEqual(
|
||||
JSON.parse(req.requestBody),
|
||||
{
|
||||
path: 'cubbyhole-test',
|
||||
type: 'cubbyhole',
|
||||
config: { id: 'cubbyhole-test', max_lease_ttl: '125h', listing_visibility: 'hidden' },
|
||||
},
|
||||
'Correct payload is sent when sending a non-wif secret engine with identity_token_key accidentally set'
|
||||
);
|
||||
return {};
|
||||
});
|
||||
const mountData = {
|
||||
id: 'cubbyhole-test',
|
||||
path: 'cubbyhole-test',
|
||||
type: 'cubbyhole',
|
||||
config: this.store.createRecord('mount-config', {
|
||||
maxLeaseTtl: '125h',
|
||||
identity_token_key: 'test-key',
|
||||
}),
|
||||
uuid: 'f1739f9d-dfc0-83c8-011f-ec17103a06c4',
|
||||
};
|
||||
const record = this.store.createRecord('secret-engine', mountData);
|
||||
await record.save();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,13 +109,37 @@ module('Unit | Model | secret-engine', function (hooks) {
|
||||
'config.allowedResponseHeaders',
|
||||
]);
|
||||
});
|
||||
|
||||
test('it returns correct fields for aws', function (assert) {
|
||||
assert.expect(1);
|
||||
const model = this.store.createRecord('secret-engine', {
|
||||
type: 'aws',
|
||||
});
|
||||
|
||||
assert.deepEqual(model.get('formFields'), [
|
||||
'type',
|
||||
'path',
|
||||
'description',
|
||||
'accessor',
|
||||
'local',
|
||||
'sealWrap',
|
||||
'config.defaultLeaseTtl',
|
||||
'config.maxLeaseTtl',
|
||||
'config.allowedManagedKeys',
|
||||
'config.auditNonHmacRequestKeys',
|
||||
'config.auditNonHmacResponseKeys',
|
||||
'config.passthroughRequestHeaders',
|
||||
'config.allowedResponseHeaders',
|
||||
'config.identityTokenKey',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
module('formFieldGroups', function () {
|
||||
test('returns correct values by default', function (assert) {
|
||||
assert.expect(1);
|
||||
const model = this.store.createRecord('secret-engine', {
|
||||
type: 'aws',
|
||||
type: 'cubbyhole',
|
||||
});
|
||||
|
||||
assert.deepEqual(model.get('formFieldGroups'), [
|
||||
@@ -261,6 +285,33 @@ module('Unit | Model | secret-engine', function (hooks) {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('returns correct values for aws', function (assert) {
|
||||
assert.expect(1);
|
||||
const model = this.store.createRecord('secret-engine', {
|
||||
type: 'aws',
|
||||
});
|
||||
|
||||
assert.deepEqual(model.get('formFieldGroups'), [
|
||||
{ default: ['path'] },
|
||||
{
|
||||
'Method Options': [
|
||||
'description',
|
||||
'config.listingVisibility',
|
||||
'local',
|
||||
'sealWrap',
|
||||
'config.defaultLeaseTtl',
|
||||
'config.maxLeaseTtl',
|
||||
'config.identityTokenKey',
|
||||
'config.allowedManagedKeys',
|
||||
'config.auditNonHmacRequestKeys',
|
||||
'config.auditNonHmacResponseKeys',
|
||||
'config.passthroughRequestHeaders',
|
||||
'config.allowedResponseHeaders',
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
module('engineType', function () {
|
||||
|
||||
22
ui/types/vault/models/aws/lease-config.d.ts
vendored
Normal file
22
ui/types/vault/models/aws/lease-config.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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
Normal file
32
ui/types/vault/models/aws/root-config.d.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
22
ui/types/vault/models/identity/oidc/config.d.ts
vendored
Normal file
22
ui/types/vault/models/identity/oidc/config.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import type Model from '@ember-data/model';
|
||||
|
||||
export default class IdentityOidcConfigModel extends Model {
|
||||
issuer: string;
|
||||
queryIssuerError: boolean;
|
||||
get attrs(): any;
|
||||
// for some reason the following Model attrs don't exist on the Model definition
|
||||
changedAttributes(): {
|
||||
[key: string]: unknown[];
|
||||
};
|
||||
rollbackAttributes(): { void };
|
||||
hasDirtyAttributes: boolean;
|
||||
isNew: boolean;
|
||||
canRead: boolean;
|
||||
save(): void;
|
||||
unloadRecord(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user