mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 01:32:33 +00:00
Allow Configuration of Azure Secret Engine, including WIF for enterprise users (#29047)
* transfer over all changes from original pr * changelog * add serialize catch for no empty string environment * move ttl format logic to parent route * Update 29047.txt * clean up some comments * Update changelog/29047.txt Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> * Update changelog/29047.txt Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> * Update ui/app/components/secret-engine/configure-azure.hbs Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> * first round of addressing pr comments, holding off on the issue save flow for error messaging to keep separate * Update CODEOWNERS merge issue * small clean up tasks * updates * test coverage * small cleanup * small clean up * clean up * clean up getters on model --------- Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
6
changelog/29047.txt
Normal file
6
changelog/29047.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
```release-note:improvement
|
||||
ui: Adds ability to edit, create, and view the Azure secrets engine configuration.
|
||||
```
|
||||
```release-note:improvement
|
||||
ui (enterprise): Allow WIF configuration on the Azure secrets engine.
|
||||
```
|
||||
@@ -23,4 +23,24 @@ export default class AzureConfig extends ApplicationAdapter {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
createOrUpdate(store, type, snapshot) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot);
|
||||
const backend = snapshot.record.backend;
|
||||
return this.ajax(this._url(backend), 'POST', { data }).then((resp) => {
|
||||
return {
|
||||
...resp,
|
||||
id: backend,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
createRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
}
|
||||
|
||||
updateRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,15 +30,12 @@
|
||||
@title="{{@typeDisplay}} not configured"
|
||||
@message="Get started by configuring your {{@typeDisplay}} secrets engine."
|
||||
>
|
||||
{{! TODO: short-term conditional to be removed once configuration for azure is merged. }}
|
||||
{{#unless (eq @typeDisplay "Azure")}}
|
||||
<Hds::Link::Standalone
|
||||
@icon="chevron-right"
|
||||
@iconPosition="trailing"
|
||||
@text="Configure {{@typeDisplay}}"
|
||||
@route="vault.cluster.secrets.backend.configuration.edit"
|
||||
@model={{@id}}
|
||||
/>
|
||||
{{/unless}}
|
||||
<Hds::Link::Standalone
|
||||
@icon="chevron-right"
|
||||
@iconPosition="trailing"
|
||||
@text="Configure {{@typeDisplay}}"
|
||||
@route="vault.cluster.secrets.backend.configuration.edit"
|
||||
@model={{@id}}
|
||||
/>
|
||||
</EmptyState>
|
||||
{{/each}}
|
||||
103
ui/app/components/secret-engine/configure-azure.hbs
Normal file
103
ui/app/components/secret-engine/configure-azure.hbs
Normal file
@@ -0,0 +1,103 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<form {{on "submit" (perform this.submitForm)}} aria-label="configure azure credentials" data-test-configure-form>
|
||||
<NamespaceReminder @mode="save" @noun="configuration" />
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
<div class="box is-fullwidth is-sideless">
|
||||
{{! accessType can be "azure" or "wif" - since WIF is an enterprise only feature we default to "azure" for community users and only display those related form fields. }}
|
||||
{{#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 Azure. Access can be configured either using an Azure account or with the
|
||||
Plugin Workload Identity Federation (WIF).
|
||||
{{/if}}
|
||||
</p>
|
||||
<div>
|
||||
<RadioButton
|
||||
id="access-type-azure"
|
||||
name="azure"
|
||||
class="radio"
|
||||
data-test-access-type="azure"
|
||||
@value="azure"
|
||||
@groupValue={{this.accessType}}
|
||||
@onChange={{fn this.onChangeAccessType "azure"}}
|
||||
@disabled={{this.disableAccessType}}
|
||||
/>
|
||||
<label for="access-type-azure">Azure account credentials</label>
|
||||
<RadioButton
|
||||
id="access-type-wif"
|
||||
name="wif"
|
||||
class="radio has-left-margin-m"
|
||||
data-test-access-type="wif"
|
||||
@value="wif"
|
||||
@groupValue={{this.accessType}}
|
||||
@onChange={{fn this.onChangeAccessType "wif"}}
|
||||
@disabled={{this.disableAccessType}}
|
||||
/>
|
||||
<label for="access-type-wif">Workload Identity Federation</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
{{/if}}
|
||||
{{#if (eq this.accessType "wif")}}
|
||||
{{! WIF Fields }}
|
||||
{{#each @issuerConfig.displayAttrs as |attr|}}
|
||||
<FormField @attr={{attr}} @model={{@issuerConfig}} />
|
||||
{{/each}}
|
||||
<FormFieldGroups @model={{@model}} @mode={{if @model.isNew "create" "edit"}} @groupName="fieldGroupsWif" />
|
||||
{{else}}
|
||||
{{! Azure Account Fields }}
|
||||
<FormFieldGroups
|
||||
@model={{@model}}
|
||||
@mode={{if @model.isNew "create" "edit"}}
|
||||
@useEnableInput={{true}}
|
||||
@groupName="fieldGroupsAzure"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@text="Save"
|
||||
@icon={{if this.save.isRunning "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-save
|
||||
/>
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
class="has-left-margin-s"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.onCancel}}
|
||||
data-test-cancel
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<AlertInline data-test-invalid-form-alert class="has-top-padding-s" @type="danger" @message={{this.invalidFormAlert}} />
|
||||
{{/if}}
|
||||
</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}}
|
||||
184
ui/app/components/secret-engine/configure-azure.ts
Normal file
184
ui/app/components/secret-engine/configure-azure.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
import type ConfigModel from 'vault/models/azure/config';
|
||||
import type IdentityOidcConfigModel from 'vault/models/identity/oidc/config';
|
||||
import type Router from '@ember/routing/router';
|
||||
import type StoreService from 'vault/services/store';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
|
||||
/**
|
||||
* @module SecretEngineConfigureAzure component is used to configure the Azure secret engine
|
||||
* For enterprise users, they will see an additional option to config WIF attributes in place of Azure account attributes.
|
||||
* If the user is configuring WIF attributes they will also have the option to update the global issuer config, which is a separate endpoint named identity/oidc/config.
|
||||
* @example
|
||||
* <SecretEngine::ConfigureAzure
|
||||
@model={{this.model.azure-config}}
|
||||
@backendPath={{this.model.id}}
|
||||
@issuerConfig={{this.model.identity-oidc-config}}
|
||||
/>
|
||||
*
|
||||
* @param {object} model - Azure config model
|
||||
* @param {string} backendPath - name of the Azure secret engine, ex: 'azure-123'
|
||||
* @param {object} issuerConfigModel - the identity/oidc/config model
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
model: ConfigModel;
|
||||
issuerConfig: IdentityOidcConfigModel;
|
||||
backendPath: string;
|
||||
}
|
||||
|
||||
export default class ConfigureAzureComponent extends Component<Args> {
|
||||
@service declare readonly router: Router;
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly version: VersionService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked accessType = 'azure';
|
||||
@tracked errorMessage = '';
|
||||
@tracked invalidFormAlert = '';
|
||||
@tracked saveIssuerWarning = '';
|
||||
|
||||
disableAccessType = false;
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
// the following checks are only relevant to existing enterprise configurations
|
||||
if (this.version.isCommunity && this.args.model.isNew) return;
|
||||
const { isWifPluginConfigured, isAzureAccountConfigured } = this.args.model;
|
||||
this.accessType = isWifPluginConfigured ? 'wif' : 'azure';
|
||||
// if there are either WIF or azure attributes, disable user's ability to change accessType
|
||||
this.disableAccessType = isWifPluginConfigured || isAzureAccountConfigured;
|
||||
}
|
||||
|
||||
get modelAttrChanged() {
|
||||
// "backend" dirties model state so explicity ignore it here
|
||||
return Object.keys(this.args.model?.changedAttributes()).some((item) => item !== 'backend');
|
||||
}
|
||||
|
||||
get issuerAttrChanged() {
|
||||
return this.args.issuerConfig?.hasDirtyAttributes;
|
||||
}
|
||||
|
||||
@action continueSubmitForm() {
|
||||
this.saveIssuerWarning = '';
|
||||
this.save.perform();
|
||||
}
|
||||
|
||||
// check if the issuer has been changed to show issuer modal
|
||||
// continue saving the configuration
|
||||
submitForm = task(
|
||||
waitFor(async (event: Event) => {
|
||||
event?.preventDefault();
|
||||
this.resetErrors();
|
||||
|
||||
if (this.issuerAttrChanged) {
|
||||
// if the issuer has changed show modal with warning that the config will change
|
||||
// if the modal is shown, the user has to click confirm to continue saving
|
||||
this.saveIssuerWarning = `You are updating the global issuer config. This will overwrite Vault's current issuer ${
|
||||
this.args.issuerConfig.queryIssuerError ? 'if it exists ' : ''
|
||||
}and may affect other configurations using this value. Continue?`;
|
||||
// exit task until user confirms
|
||||
return;
|
||||
}
|
||||
await this.save.perform();
|
||||
})
|
||||
);
|
||||
|
||||
save = task(
|
||||
waitFor(async () => {
|
||||
const modelAttrChanged = this.modelAttrChanged;
|
||||
const issuerAttrChanged = this.issuerAttrChanged;
|
||||
// check if any of the model or issue attributes have changed
|
||||
// if no changes, transition and notify user
|
||||
if (!modelAttrChanged && !issuerAttrChanged) {
|
||||
this.flashMessages.info('No changes detected.');
|
||||
this.transition();
|
||||
return;
|
||||
}
|
||||
|
||||
const modelSaved = modelAttrChanged ? await this.saveModel() : false;
|
||||
const issuerSaved = issuerAttrChanged ? await this.updateIssuer() : false;
|
||||
|
||||
if (modelSaved || (!modelAttrChanged && issuerSaved)) {
|
||||
// transition if the model was saved successfully
|
||||
// we only prevent a transition if the model is edited and fails saving
|
||||
this.transition();
|
||||
} else {
|
||||
// otherwise there was a failure and we should not transition and exit the function
|
||||
return;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
async updateIssuer(): Promise<boolean> {
|
||||
try {
|
||||
await this.args.issuerConfig.save();
|
||||
this.flashMessages.success('Issuer saved successfully');
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.flashMessages.danger(`Issuer was not saved: ${errorMessage(e, 'Check Vault logs for details.')}`);
|
||||
// remove issuer from the config model if it was not saved
|
||||
this.args.issuerConfig.rollbackAttributes();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveModel(): Promise<boolean> {
|
||||
const { backendPath, model } = this.args;
|
||||
try {
|
||||
await model.save();
|
||||
this.flashMessages.success(`Successfully saved ${backendPath}'s configuration.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.errorMessage = errorMessage(error);
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
resetErrors() {
|
||||
this.flashMessages.clearMessages();
|
||||
this.errorMessage = '';
|
||||
this.invalidFormAlert = '';
|
||||
}
|
||||
|
||||
transition() {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath);
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeAccessType(accessType: string) {
|
||||
this.accessType = accessType;
|
||||
const { model } = this.args;
|
||||
if (accessType === 'azure') {
|
||||
// reset all WIF attributes
|
||||
model.identityTokenAudience = model.identityTokenTtl = undefined;
|
||||
// return the issuer to the globally set value (if there is one) on toggle
|
||||
this.args.issuerConfig.rollbackAttributes();
|
||||
}
|
||||
if (accessType === 'wif') {
|
||||
// reset all Azure attributes
|
||||
model.clientSecret = model.rootPasswordTtl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onCancel() {
|
||||
this.resetErrors();
|
||||
this.args.model.unloadRecord();
|
||||
this.transition();
|
||||
}
|
||||
}
|
||||
@@ -153,6 +153,10 @@ export function configurationOnly() {
|
||||
// These engines do not exist in their own Ember engine.
|
||||
export const CONFIGURABLE_SECRET_ENGINES = ['aws', 'azure', 'ssh'];
|
||||
|
||||
export function configurableSecretEngines() {
|
||||
return CONFIGURABLE_SECRET_ENGINES.slice();
|
||||
}
|
||||
|
||||
export function mountableEngines() {
|
||||
return MOUNTABLE_SECRET_ENGINES.slice();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
|
||||
// Note: while the API docs indicate subscriptionId and tenantId are required, the UI does not enforce this because the user may pass these values in as environment variables.
|
||||
// https://developer.hashicorp.com/vault/api-docs/secret/azure#configure-access
|
||||
@@ -50,19 +50,59 @@ export default class AzureConfig extends Model {
|
||||
'environment',
|
||||
];
|
||||
|
||||
// for configuration details view
|
||||
// do not include clientSecret because it is never returned by the API
|
||||
get displayAttrs() {
|
||||
return this.formFields.filter((attr) => attr.name !== 'clientSecret');
|
||||
}
|
||||
|
||||
/* GETTERS used by configure-azure component
|
||||
these getters help:
|
||||
1. determine if the model is new or existing
|
||||
2. if wif or azure attributes have been configured
|
||||
*/
|
||||
get isConfigured() {
|
||||
// if every value is falsy, this engine has not been configured yet
|
||||
return !this.configurableParams.every((param) => !this[param]);
|
||||
}
|
||||
|
||||
// formFields are iterated through to generate the edit/create view
|
||||
get formFields() {
|
||||
return expandAttributeMeta(this, this.configurableParams);
|
||||
get isWifPluginConfigured() {
|
||||
return !!this.identityTokenAudience || !!this.identityTokenTtl;
|
||||
}
|
||||
|
||||
get isAzureAccountConfigured() {
|
||||
// clientSecret is not checked here because it's never return by the API
|
||||
// however it is an Azure account field
|
||||
return !!this.rootPasswordTtl;
|
||||
}
|
||||
|
||||
/* GETTERS used to generate array of fields to be displayed in:
|
||||
1. details view
|
||||
2. edit/create view
|
||||
*/
|
||||
get displayAttrs() {
|
||||
const formFields = expandAttributeMeta(this, this.configurableParams);
|
||||
return formFields.filter((attr) => attr.name !== 'clientSecret');
|
||||
}
|
||||
|
||||
// "filedGroupsWif" and "fieldGroupsAzure" are passed to the FormFieldGroups component to determine which group to show in the form (ex: @groupName="fieldGroupsWif")
|
||||
get fieldGroupsWif() {
|
||||
return fieldToAttrs(this, this.formFieldGroups('wif'));
|
||||
}
|
||||
|
||||
get fieldGroupsAzure() {
|
||||
return fieldToAttrs(this, this.formFieldGroups('azure'));
|
||||
}
|
||||
|
||||
formFieldGroups(accessType = 'azure') {
|
||||
const formFieldGroups = [];
|
||||
formFieldGroups.push({
|
||||
default: ['subscriptionId', 'tenantId', 'clientId', 'environment'],
|
||||
});
|
||||
if (accessType === 'wif') {
|
||||
formFieldGroups.push({
|
||||
default: ['identityTokenAudience', 'identityTokenTtl'],
|
||||
});
|
||||
}
|
||||
if (accessType === 'azure') {
|
||||
formFieldGroups.push({
|
||||
default: ['clientSecret', 'rootPasswordTtl'],
|
||||
});
|
||||
}
|
||||
return formFieldGroups;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import type VersionService from 'vault/services/version';
|
||||
|
||||
const CONFIG_ADAPTERS_PATHS: Record<string, string[]> = {
|
||||
aws: ['aws/lease-config', 'aws/root-config'],
|
||||
azure: ['azure/config'],
|
||||
ssh: ['ssh/ca-config'],
|
||||
};
|
||||
|
||||
@@ -47,10 +48,21 @@ export default class SecretsBackendConfigurationEdit extends Route {
|
||||
// ex: adapterPath = ssh/ca-config, convert to: ssh-ca-config so that you can pass to component @model={{this.model.ssh-ca-config}}
|
||||
const standardizedKey = adapterPath.replace(/\//g, '-');
|
||||
try {
|
||||
model[standardizedKey] = await this.store.queryRecord(adapterPath, {
|
||||
const configModel = await this.store.queryRecord(adapterPath, {
|
||||
backend,
|
||||
type,
|
||||
});
|
||||
// some of the models return a 200 if they are not configured (ex: azure)
|
||||
// so instead of checking a catch or httpStatus, we check if the model is configured based on the getter `isConfigured` on the engine's model
|
||||
// if the engine is not configured we update the record to get the default values
|
||||
if (!configModel.isConfigured && type === 'azure') {
|
||||
model[standardizedKey] = await this.store.createRecord(adapterPath, {
|
||||
backend,
|
||||
type,
|
||||
});
|
||||
} else {
|
||||
model[standardizedKey] = configModel;
|
||||
}
|
||||
} catch (e: AdapterError) {
|
||||
// For most models if the adapter returns a 404, we want to create a new record.
|
||||
// The ssh secret engine however returns a 400 if the CA is not configured.
|
||||
|
||||
@@ -35,6 +35,12 @@
|
||||
@issuerConfig={{this.model.identity-oidc-config}}
|
||||
@backendPath={{this.model.id}}
|
||||
/>
|
||||
{{else if (eq this.model.type "azure")}}
|
||||
<SecretEngine::ConfigureAzure
|
||||
@model={{this.model.azure-config}}
|
||||
@backendPath={{this.model.id}}
|
||||
@issuerConfig={{this.model.identity-oidc-config}}
|
||||
/>
|
||||
{{else if (eq this.model.type "ssh")}}
|
||||
<SecretEngine::ConfigureSsh @model={{this.model.ssh-ca-config}} @id={{this.model.id}} />
|
||||
{{/if}}
|
||||
@@ -6,20 +6,17 @@
|
||||
<SecretListHeader @model={{this.model.secretEngineModel}} @isConfigure={{true}} />
|
||||
|
||||
{{#if this.isConfigurable}}
|
||||
{{! TODO: short-term conditional to be removed once configuration for azure is merged. }}
|
||||
{{#unless (eq this.typeDisplay "Azure")}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
<ToolbarLink
|
||||
@route="vault.cluster.secrets.backend.configuration.edit"
|
||||
@model={{this.model.secretEngineModel.id}}
|
||||
data-test-secret-backend-configure
|
||||
>
|
||||
Configure
|
||||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
{{/unless}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
<ToolbarLink
|
||||
@route="vault.cluster.secrets.backend.configuration.edit"
|
||||
@model={{this.model.secretEngineModel.id}}
|
||||
data-test-secret-backend-configure
|
||||
>
|
||||
Configure
|
||||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<SecretEngine::ConfigurationDetails
|
||||
@configModels={{this.model.configModels}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { click, visit, currentURL } from '@ember/test-helpers';
|
||||
import { click, visit, currentURL, fillIn } from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
expectedConfigKeys,
|
||||
expectedValueOfConfigKeys,
|
||||
configUrl,
|
||||
createConfig,
|
||||
fillInAzureConfig,
|
||||
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
|
||||
module('Acceptance | Azure | configuration', function (hooks) {
|
||||
@@ -31,113 +33,427 @@ module('Acceptance | Azure | configuration', function (hooks) {
|
||||
const flash = this.owner.lookup('service:flash-messages');
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.flashSuccessSpy = spy(flash, 'success');
|
||||
this.flashDangerSpy = spy(flash, 'danger');
|
||||
this.flashInfoSpy = spy(flash, 'info');
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.uid = uuidv4();
|
||||
this.type = 'azure';
|
||||
return authPage.login();
|
||||
});
|
||||
|
||||
test('it should prompt configuration after mounting the azure engine', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
await visit('/vault/settings/mount-secret-backend');
|
||||
await mountBackend(this.type, path);
|
||||
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/configuration`,
|
||||
'navigated to configuration view'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.emptyStateTitle)
|
||||
.hasText('Azure not configured', "empty state title is 'Azure not configured'");
|
||||
assert.dom(GENERAL.emptyStateActions).hasText('Configure Azure');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should transition to configure page on click "Configure" from toolbar', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
await enablePage.enable(this.type, path);
|
||||
await click(SES.configure);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/configuration/edit`,
|
||||
'navigated to configuration edit view'
|
||||
);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
module('isCommunity', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'community';
|
||||
});
|
||||
|
||||
module('details', function () {
|
||||
test('it should show configuration with Azure account options configured', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
const azureAccountAttrs = {
|
||||
client_secret: 'client-secret',
|
||||
subscription_id: 'subscription-id',
|
||||
tenant_id: 'tenant-id',
|
||||
client_id: 'client-id',
|
||||
root_password_ttl: '20 days 20 hours',
|
||||
environment: 'AZUREPUBLICCLOUD',
|
||||
};
|
||||
this.server.get(`${path}/config`, () => {
|
||||
assert.ok(true, 'request made to config when navigating to the configuration page.');
|
||||
return { data: { id: path, type: this.type, ...azureAccountAttrs } };
|
||||
});
|
||||
await enablePage.enable(this.type, path);
|
||||
for (const key of expectedConfigKeys('azure')) {
|
||||
assert.dom(GENERAL.infoRowLabel(key)).exists(`${key} on the ${this.type} config details exists.`);
|
||||
const responseKeyAndValue = expectedValueOfConfigKeys(this.type, key);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue(key))
|
||||
.hasText(responseKeyAndValue, `value for ${key} on the ${this.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 show API error when configuration read fails', async function (assert) {
|
||||
assert.expect(1);
|
||||
const path = `azure-${this.uid}`;
|
||||
// interrupt get and return API error
|
||||
this.server.get(configUrl(this.type, path), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
await enablePage.enable(this.type, path);
|
||||
assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route');
|
||||
});
|
||||
});
|
||||
|
||||
module('create', function () {
|
||||
test('it should save azure account accessType options', async function (assert) {
|
||||
assert.expect(3);
|
||||
const path = `azure-${this.uid}`;
|
||||
await enablePage.enable(this.type, path);
|
||||
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
assert.notOk(
|
||||
true,
|
||||
'post request was made to issuer endpoint when on community and data not changed. test should fail.'
|
||||
);
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAzureConfig(this.type);
|
||||
await click(GENERAL.saveButton);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s configuration.`),
|
||||
'Success flash message is rendered showing the azure model configuration was saved.'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Root password TTL'))
|
||||
.hasText(
|
||||
'1 hour 26 minutes 40 seconds',
|
||||
'Root password TTL, an azure account specific field, has been set.'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Subscription ID'))
|
||||
.hasText('subscription-id', 'Subscription ID, a generic field, has been set.');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
});
|
||||
|
||||
module('edit', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
const path = `azure-${this.uid}`;
|
||||
const type = 'azure';
|
||||
const genericAttrs = {
|
||||
// attributes that can be used for either wif or azure account access type
|
||||
subscription_id: 'subscription-id',
|
||||
tenant_id: 'tenant-id',
|
||||
client_id: 'client-id',
|
||||
environment: 'AZUREPUBLICCLOUD',
|
||||
};
|
||||
this.server.get(`${path}/config`, () => {
|
||||
return { data: { id: path, type, ...genericAttrs } };
|
||||
});
|
||||
await enablePage.enable(type, path);
|
||||
});
|
||||
|
||||
test('it should not save client secret if it has NOT been changed', async function (assert) {
|
||||
assert.expect(2);
|
||||
await click(SES.configure);
|
||||
const url = currentURL();
|
||||
const path = url.split('/')[3]; // get path from url because we can't pass the path from beforeEach hook to individual test.
|
||||
this.server.post(configUrl('azure', path), (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(
|
||||
undefined,
|
||||
payload.client_secret,
|
||||
'client_secret is not sent if it has not been changed'
|
||||
);
|
||||
assert.strictEqual(
|
||||
payload.subscription_id,
|
||||
'subscription-id-updated',
|
||||
'subscription_id is included with updated value in the payload'
|
||||
);
|
||||
});
|
||||
await fillIn(GENERAL.inputByAttr('subscriptionId'), 'subscription-id-updated');
|
||||
await click(GENERAL.enableField('clientSecret'));
|
||||
await click(GENERAL.saveButton);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should save client secret if it HAS been changed', async function (assert) {
|
||||
assert.expect(2);
|
||||
await click(SES.configure);
|
||||
const url = currentURL();
|
||||
const path = url.split('/')[3]; // get path from url because we can't pass the path from beforeEach hook to individual test.
|
||||
this.server.post(configUrl('azure', path), (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(
|
||||
payload.client_secret,
|
||||
'client-secret-updated',
|
||||
'client_secret is sent on payload because user updated its value'
|
||||
);
|
||||
assert.strictEqual(
|
||||
payload.subscription_id,
|
||||
'subscription-id-updated-again',
|
||||
'subscription_id is included with updated value in the payload'
|
||||
);
|
||||
});
|
||||
await fillIn(GENERAL.inputByAttr('subscriptionId'), 'subscription-id-updated-again');
|
||||
await click(GENERAL.enableField('clientSecret'));
|
||||
await click('[data-test-button="toggle-masked"]');
|
||||
await fillIn(GENERAL.inputByAttr('clientSecret'), 'client-secret-updated');
|
||||
await click(GENERAL.saveButton);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
test('it should show empty state and navigate to configuration view after mounting the azure engine', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
await visit('/vault/settings/mount-secret-backend');
|
||||
await mountBackend('azure', path);
|
||||
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/configuration`,
|
||||
'navigated to configuration view'
|
||||
);
|
||||
assert.dom(GENERAL.emptyStateTitle).hasText('Azure not configured');
|
||||
assert.dom(GENERAL.emptyStateActions).doesNotContainText('Configure Azure');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should not show "Configure" from toolbar', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
await enablePage.enable('azure', path);
|
||||
assert.dom(SES.configure).doesNotExist('Configure button does not exist.');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should show configuration with WIF options configured', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
const type = 'azure';
|
||||
const wifAttrs = {
|
||||
subscription_id: 'subscription-id',
|
||||
tenant_id: 'tenant-id',
|
||||
client_id: 'client-id',
|
||||
identity_token_audience: 'audience',
|
||||
identity_token_ttl: 720000,
|
||||
environment: 'AZUREPUBLICCLOUD',
|
||||
};
|
||||
this.server.get(`${path}/config`, () => {
|
||||
assert.ok(true, 'request made to config when navigating to the configuration page.');
|
||||
return { data: { id: path, type, ...wifAttrs } };
|
||||
});
|
||||
await enablePage.enable(type, path);
|
||||
for (const key of expectedConfigKeys('azure-wif')) {
|
||||
const responseKeyAndValue = expectedValueOfConfigKeys(type, key);
|
||||
module('details', function () {
|
||||
test('it should save WIF configuration options', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
const wifAttrs = {
|
||||
subscription_id: 'subscription-id',
|
||||
tenant_id: 'tenant-id',
|
||||
client_id: 'client-id',
|
||||
identity_token_audience: 'audience',
|
||||
identity_token_ttl: 720000,
|
||||
environment: 'AZUREPUBLICCLOUD',
|
||||
};
|
||||
this.server.get(`${path}/config`, () => {
|
||||
assert.ok(true, 'request made to config when navigating to the configuration page.');
|
||||
return { data: { id: path, type: this.type, ...wifAttrs } };
|
||||
});
|
||||
await enablePage.enable(this.type, path);
|
||||
for (const key of expectedConfigKeys('azure-wif')) {
|
||||
const responseKeyAndValue = expectedValueOfConfigKeys(this.type, key);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue(key))
|
||||
.hasText(responseKeyAndValue, `value for ${key} on the ${this.type} config details exists.`);
|
||||
}
|
||||
// check mount configuration details are present and accurate.
|
||||
await click(SES.configurationToggle);
|
||||
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}`);
|
||||
.dom(GENERAL.infoRowValue('Path'))
|
||||
.hasText(`${path}/`, 'mount path is displayed in the configuration details');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should not show issuer if no WIF configuration data is returned', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
this.server.get(`${path}/config`, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.ok(true, 'request made to config/root when navigating to the configuration page.');
|
||||
return { data: { id: path, type: this.type, attributes: payload } };
|
||||
});
|
||||
this.server.get(`identity/oidc/config`, () => {
|
||||
assert.notOk(true, 'request made to return issuer. test should fail.');
|
||||
});
|
||||
await createConfig(this.store, path, this.type); // create the azure account config in the store
|
||||
await enablePage.enable(this.type, path);
|
||||
await click(SES.configTab);
|
||||
|
||||
assert.dom(GENERAL.infoRowLabel('Issuer')).doesNotExist(`Issuer does not exists on config details.`);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('it should show configuration with Azure account options configured', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
const type = 'azure';
|
||||
const azureAccountAttrs = {
|
||||
client_secret: 'client-secret',
|
||||
subscription_id: 'subscription-id',
|
||||
tenant_id: 'tenant-id',
|
||||
client_id: 'client-id',
|
||||
root_password_ttl: '20 days 20 hours',
|
||||
environment: 'AZUREPUBLICCLOUD',
|
||||
};
|
||||
this.server.get(`${path}/config`, () => {
|
||||
assert.ok(true, 'request made to config when navigating to the configuration page.');
|
||||
return { data: { id: path, type, ...azureAccountAttrs } };
|
||||
});
|
||||
await enablePage.enable(type, path);
|
||||
for (const key of expectedConfigKeys('azure')) {
|
||||
assert.dom(GENERAL.infoRowLabel(key)).exists(`${key} on the ${type} config details exists.`);
|
||||
const responseKeyAndValue = expectedValueOfConfigKeys(type, key);
|
||||
module('create', function () {
|
||||
test('it should transition and save issuer if model was not changed but issuer was', async function (assert) {
|
||||
assert.expect(3);
|
||||
const path = `azure-${this.uid}`;
|
||||
await enablePage.enable(this.type, path);
|
||||
const newIssuer = `http://new.issuer.${this.uid}`;
|
||||
|
||||
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',
|
||||
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(this.type, path), () => {
|
||||
throw new Error('post request was incorrectly made to update the azure config model');
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await click(SES.wif.accessType('wif'));
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), newIssuer);
|
||||
await click(GENERAL.saveButton);
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Issuer saved successfully`),
|
||||
'Shows issuer saved message'
|
||||
);
|
||||
|
||||
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}`);
|
||||
.dom(GENERAL.emptyStateTitle)
|
||||
.hasText(
|
||||
'Azure not configured',
|
||||
'Empty state message is displayed because the model was not saved only the issuer'
|
||||
);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should transition and save model if the model was changed but issuer was not', async function (assert) {
|
||||
assert.expect(4);
|
||||
const path = `azure-${this.uid}`;
|
||||
await enablePage.enable(this.type, path);
|
||||
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
throw new Error('post request was incorrectly made to update the issuer');
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAzureConfig('withWif');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningModal).doesNotExist('issuer warning modal does not show');
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s configuration.`),
|
||||
'Success flash message is rendered showing the azure model configuration was saved.'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Identity token audience'))
|
||||
.hasText('azure-audience', 'Identity token audience has been set.');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Identity token TTL'))
|
||||
.hasText('2 hours', 'Identity token TTL has been set.');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should transition and show issuer error if model saved but issuer encountered an error', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
const oldIssuer = 'http://old.issuer';
|
||||
await enablePage.enable(this.type, path);
|
||||
this.server.get('/identity/oidc/config', () => {
|
||||
return { issuer: 'http://old.issuer' };
|
||||
});
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAzureConfig('withWif');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(oldIssuer, 'issuer defaults to previously saved value');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://new.issuererrors');
|
||||
await click(GENERAL.saveButton);
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s configuration.`),
|
||||
'Success flash message is rendered showing the azure model configuration was saved.'
|
||||
);
|
||||
assert.true(
|
||||
this.flashDangerSpy.calledWith(`Issuer was not saved: bad request`),
|
||||
'Danger flash message is rendered showing the issuer was not saved.'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Identity token audience'))
|
||||
.hasText('azure-audience', 'Identity token audience has been set.');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Identity token TTL'))
|
||||
.hasText('2 hours', 'Identity token TTL has been set.');
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Issuer'))
|
||||
.hasText(oldIssuer, 'Issuer is shows the old saved value not the new value that errors on save.');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
|
||||
test('it should NOT transition and show error if model errored but issuer was saved', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
const newIssuer = `http://new.issuer.${this.uid}`;
|
||||
const oldIssuer = 'http://old.issuer';
|
||||
await enablePage.enable(this.type, path);
|
||||
this.server.get('/identity/oidc/config', () => {
|
||||
return { issuer: oldIssuer };
|
||||
});
|
||||
this.server.post(configUrl(this.type, path), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
});
|
||||
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAzureConfig('withWif');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(oldIssuer, 'issuer defaults to previously saved value');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), newIssuer);
|
||||
await click(GENERAL.saveButton);
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Issuer saved successfully`),
|
||||
'Success flash message is rendered showing the issuer configuration was saved.'
|
||||
);
|
||||
assert.dom(GENERAL.messageError).hasText('Error bad request', 'Error message is displayed.');
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/configuration/edit`,
|
||||
'stays on the edit page'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(newIssuer, 'issuer is updated to newly saved value');
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('it should show API error when configuration read fails', async function (assert) {
|
||||
assert.expect(1);
|
||||
const path = `azure-${this.uid}`;
|
||||
const type = 'azure';
|
||||
// interrupt get and return API error
|
||||
this.server.get(configUrl(type, path), () => {
|
||||
return overrideResponse(400, { errors: ['bad request'] });
|
||||
module('edit', function () {
|
||||
test('it should update WIF attributes', async function (assert) {
|
||||
const path = `azure-${this.uid}`;
|
||||
await enablePage.enable(this.type, path);
|
||||
await click(SES.configTab);
|
||||
await click(SES.configure);
|
||||
await fillInAzureConfig('withWif');
|
||||
await click(GENERAL.saveButton); // finished creating attributes, go back and edit them.
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Identity token audience'))
|
||||
.hasText('azure-audience', `value for identity token audience shows on the config details view.`);
|
||||
await click(SES.configure);
|
||||
await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'new-audience');
|
||||
await click(GENERAL.saveButton);
|
||||
assert
|
||||
.dom(GENERAL.infoRowValue('Identity token audience'))
|
||||
.hasText('new-audience', `value for identity token audience shows on the config details view.`);
|
||||
// cleanup
|
||||
await runCmd(`delete sys/mounts/${path}`);
|
||||
});
|
||||
await enablePage.enable(type, path);
|
||||
assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ const createSshCaConfig = (store, backend) => {
|
||||
return store.peekRecord('ssh/ca-config', backend);
|
||||
};
|
||||
|
||||
const createAzureConfig = (store, backend, accessType) => {
|
||||
const createAzureConfig = (store, backend, accessType = 'generic') => {
|
||||
// clear any records first
|
||||
// note: allowed "environment" params for testing https://github.com/hashicorp/vault-plugin-secrets-azure/blob/main/client.go#L35-L37
|
||||
store.unloadAll('azure/config');
|
||||
@@ -117,6 +117,21 @@ const createAzureConfig = (store, backend, accessType) => {
|
||||
environment: 'AZUREPUBLICCLOUD',
|
||||
},
|
||||
});
|
||||
} else if (accessType === 'wif') {
|
||||
store.pushPayload('azure/config', {
|
||||
id: backend,
|
||||
modelName: 'azure/config',
|
||||
data: {
|
||||
backend,
|
||||
subscription_id: 'subscription-id',
|
||||
tenant_id: 'tenant-id',
|
||||
client_id: 'client-id',
|
||||
identity_token_audience: 'audience',
|
||||
identity_token_ttl: 7200,
|
||||
root_password_ttl: '20 days 20 hours',
|
||||
environment: 'AZUREPUBLICCLOUD',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
store.pushPayload('azure/config', {
|
||||
id: backend,
|
||||
@@ -162,6 +177,10 @@ export const createConfig = (store, backend, type) => {
|
||||
return createSshCaConfig(store, backend);
|
||||
case 'azure':
|
||||
return createAzureConfig(store, backend, 'azure');
|
||||
case 'azure-wif':
|
||||
return createAzureConfig(store, backend, 'wif');
|
||||
case 'azure-generic':
|
||||
return createAzureConfig(store, backend, 'generic');
|
||||
}
|
||||
};
|
||||
// Used in tests to assert the expected keys in the config details of configurable secret engines
|
||||
@@ -286,6 +305,25 @@ export const fillInAwsConfig = async (situation = 'withAccess') => {
|
||||
}
|
||||
};
|
||||
|
||||
export const fillInAzureConfig = async (situation = 'azure') => {
|
||||
await fillIn(GENERAL.inputByAttr('subscriptionId'), 'subscription-id');
|
||||
await fillIn(GENERAL.inputByAttr('tenantId'), 'tenant-id');
|
||||
await fillIn(GENERAL.inputByAttr('clientId'), 'client-id');
|
||||
await fillIn(GENERAL.inputByAttr('environment'), 'AZUREPUBLICCLOUD');
|
||||
|
||||
if (situation === 'azure') {
|
||||
await fillIn(GENERAL.inputByAttr('clientSecret'), 'client-secret');
|
||||
await click(GENERAL.ttl.toggle('Root password TTL'));
|
||||
await fillIn(GENERAL.ttl.input('Root password TTL'), '5200');
|
||||
}
|
||||
if (situation === 'withWif') {
|
||||
await click(SES.wif.accessType('wif')); // toggle to wif
|
||||
await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'azure-audience');
|
||||
await click(GENERAL.ttl.toggle('Identity token TTL'));
|
||||
await fillIn(GENERAL.ttl.input('Identity token TTL'), '7200');
|
||||
}
|
||||
};
|
||||
|
||||
// Example usage
|
||||
// createLongJson (2, 3) will create a json object with 2 original keys, each with 3 nested keys
|
||||
// {
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import sinon from 'sinon';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import { render, click, fillIn } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
import {
|
||||
expectedConfigKeys,
|
||||
createConfig,
|
||||
configUrl,
|
||||
fillInAzureConfig,
|
||||
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
|
||||
import { capabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
|
||||
module('Integration | Component | SecretEngine/ConfigureAzure', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.flashMessages = this.owner.lookup('service:flash-messages');
|
||||
this.flashMessages.registerTypes(['success', 'danger']);
|
||||
this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success');
|
||||
this.flashDangerSpy = sinon.spy(this.flashMessages, 'danger');
|
||||
this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
|
||||
this.uid = uuidv4();
|
||||
this.id = `azure-${this.uid}`;
|
||||
this.config = this.store.createRecord('azure/config');
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.config.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url)
|
||||
// stub capabilities so that by default user can read and update issuer
|
||||
this.server.post('/sys/capabilities-self', () => capabilitiesStub('identity/oidc/config', ['sudo']));
|
||||
|
||||
this.renderComponent = () => {
|
||||
return render(hbs`
|
||||
<SecretEngine::ConfigureAzure @model={{this.config}} @issuerConfig={{this.issuerConfig}} @backendPath={{this.id}} />
|
||||
`);
|
||||
};
|
||||
});
|
||||
module('Create view', function () {
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
test('it renders default fields, showing access type options for enterprise users', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.configureForm).exists('it lands on the Azure configuration form.');
|
||||
assert.dom(SES.wif.accessType('azure')).isChecked('defaults to showing Azure access type checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isNotChecked('wif access type is not checked');
|
||||
// check all the form fields are present
|
||||
for (const key of expectedConfigKeys('azure-camelCase')) {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section`);
|
||||
}
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders wif fields when user selects wif access type', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
// check for the wif fields only
|
||||
for (const key of expectedConfigKeys('azure-wif-camelCase')) {
|
||||
if (key === 'Identity token TTL') {
|
||||
assert.dom(GENERAL.ttl.toggle(key)).exists(`${key} shows for wif section.`);
|
||||
} else {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for wif section.`);
|
||||
}
|
||||
}
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).exists('issuer shows for wif section.');
|
||||
});
|
||||
|
||||
test('it clears wif/azure-account inputs after toggling accessType', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await fillInAzureConfig('azure');
|
||||
await click(SES.wif.accessType('wif'));
|
||||
await fillInAzureConfig('withWif');
|
||||
await click(SES.wif.accessType('azure'));
|
||||
|
||||
assert
|
||||
.dom(GENERAL.toggleInput('Root password TTL'))
|
||||
.isNotChecked('rootPasswordTtl is cleared after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('clientSecret'))
|
||||
.hasValue('', 'clientSecret is cleared after toggling accessType');
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue('', 'issuer shows no value after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasAttribute(
|
||||
'placeholder',
|
||||
'https://vault-test.com',
|
||||
'issuer shows no value after toggling accessType'
|
||||
);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('identityTokenAudience'))
|
||||
.hasValue('', 'idTokenAudience is cleared after toggling accessType');
|
||||
assert
|
||||
.dom(GENERAL.toggleInput('Identity token TTL'))
|
||||
.isNotChecked('identityTokenTtl is cleared after toggling accessType');
|
||||
});
|
||||
|
||||
test('it does not clear global issuer when toggling accessType', async function (assert) {
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(this.issuerConfig.issuer, 'issuer is what is sent in by the model on first load');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://ive-changed');
|
||||
await click(SES.wif.accessType('azure'));
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(
|
||||
this.issuerConfig.issuer,
|
||||
'issuer value is still the same global value after toggling accessType'
|
||||
);
|
||||
});
|
||||
|
||||
test('it transitions without sending a config or issuer payload on cancel', async function (assert) {
|
||||
assert.expect(3);
|
||||
await this.renderComponent();
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
assert.notOk(
|
||||
true,
|
||||
'post request was made to config when user canceled out of flow. test should fail.'
|
||||
);
|
||||
});
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
assert.notOk(
|
||||
true,
|
||||
'post request was made to save issuer when user canceled out of flow. test should fail.'
|
||||
);
|
||||
});
|
||||
await fillInAzureConfig('withWif');
|
||||
await click(GENERAL.cancelButton);
|
||||
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.');
|
||||
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
module('issuer field tests', function () {
|
||||
test('if issuer API error and user changes issuer value, shows specific warning message', async function (assert) {
|
||||
this.issuerConfig.queryIssuerError = true;
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://change.me.no.read');
|
||||
await click(GENERAL.saveButton);
|
||||
assert
|
||||
.dom(SES.wif.issuerWarningMessage)
|
||||
.hasText(
|
||||
`You are updating the global issuer config. This will overwrite Vault's current issuer if it exists and may affect other configurations using this value. Continue?`,
|
||||
'modal shows message about overwriting value if it exists'
|
||||
);
|
||||
});
|
||||
|
||||
test('is shows placeholder issuer, and does not call APIs on canceling out of issuer modal', async function (assert) {
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
assert.notOk(true, 'request should not be made to issuer config endpoint');
|
||||
});
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
assert.notOk(
|
||||
true,
|
||||
'post request was made to config/ when user canceled out of flow. test should fail.'
|
||||
);
|
||||
});
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasAttribute('placeholder', 'https://vault-test.com', 'shows issuer placeholder');
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'shows issuer is empty when not passed');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), 'http://bar.foo');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningMessage).exists('issuer modal exists');
|
||||
assert
|
||||
.dom(SES.wif.issuerWarningMessage)
|
||||
.hasText(
|
||||
`You are updating the global issuer config. This will overwrite Vault's current issuer and may affect other configurations using this value. Continue?`,
|
||||
'modal shows message about overwriting value without the noRead: "if it exists" adage'
|
||||
);
|
||||
await click(SES.wif.issuerWarningCancel);
|
||||
assert.dom(SES.wif.issuerWarningMessage).doesNotExist('issuer modal is removed on cancel');
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.');
|
||||
assert.true(this.transitionStub.notCalled, 'Does not redirect');
|
||||
});
|
||||
|
||||
test('it shows modal when updating issuer and calls correct APIs on save', async function (assert) {
|
||||
const newIssuer = `http://bar.${uuidv4()}`;
|
||||
this.server.post('/identity/oidc/config', (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(payload, { issuer: newIssuer }, 'payload for issuer is correct');
|
||||
return {
|
||||
id: 'identity-oidc-config', // id needs to match the id on secret-engine-helpers createIssuerConfig
|
||||
data: null,
|
||||
warnings: [
|
||||
'If "issuer" is set explicitly, all tokens must be validated against that address, including those issued by secondary clusters. Setting issuer to "" will restore the default behavior of using the cluster\'s api_addr as the issuer.',
|
||||
],
|
||||
};
|
||||
});
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
assert.notOk(true, 'skips request to config because the model was not changed');
|
||||
});
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'issuer defaults to empty string');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), newIssuer);
|
||||
await click(GENERAL.saveButton);
|
||||
|
||||
assert.dom(SES.wif.issuerWarningMessage).exists('issue warning modal exists');
|
||||
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.');
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith('Issuer saved successfully'),
|
||||
'Success flash message called for Azure issuer'
|
||||
);
|
||||
assert.true(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows modal when modifying the issuer, has correct payload, and shows flash message on fail', async function (assert) {
|
||||
assert.expect(7);
|
||||
this.issuer = 'http://foo.bar';
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
assert.true(
|
||||
true,
|
||||
'post request was made to azure config when unsetting the issuer. test should pass.'
|
||||
);
|
||||
});
|
||||
this.server.post('/identity/oidc/config', (_, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.deepEqual(payload, { issuer: this.issuer }, 'correctly sets the issuer');
|
||||
return overrideResponse(403);
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await click(SES.wif.accessType('wif'));
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).hasValue('');
|
||||
await fillIn(GENERAL.inputByAttr('issuer'), this.issuer);
|
||||
await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'some-value');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningMessage).exists('issuer warning modal exists');
|
||||
await click(SES.wif.issuerWarningSave);
|
||||
|
||||
assert.true(
|
||||
this.flashDangerSpy.calledWith('Issuer was not saved: permission denied'),
|
||||
'shows danger flash for issuer save'
|
||||
);
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`),
|
||||
"calls the config flash message not the issuer's"
|
||||
);
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
module('isCommunity', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'community';
|
||||
});
|
||||
|
||||
test('it renders fields', async function (assert) {
|
||||
assert.expect(8);
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.configureForm).exists('t lands on the Azure configuration form');
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSection)
|
||||
.doesNotExist('Access type section does not render for a community user');
|
||||
// check all the form fields are present
|
||||
for (const key of expectedConfigKeys('azure-camelCase')) {
|
||||
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for azure account creds section.`);
|
||||
}
|
||||
assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist();
|
||||
});
|
||||
|
||||
test('it does not send issuer on save', async function (assert) {
|
||||
assert.expect(4);
|
||||
await this.renderComponent();
|
||||
this.server.post(configUrl('azure', this.id), () => {
|
||||
assert.true(true, 'post request was made to config. test should pass.');
|
||||
});
|
||||
this.server.post('/identity/oidc/config', () => {
|
||||
throw new Error('post request was incorrectly made to update issuer');
|
||||
});
|
||||
await fillInAzureConfig('azure');
|
||||
await click(GENERAL.saveButton);
|
||||
assert.dom(SES.wif.issuerWarningMessage).doesNotExist('modal should not render');
|
||||
assert.true(
|
||||
this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`),
|
||||
'Flash message shows that config was saved even if issuer was not.'
|
||||
);
|
||||
assert.ok(
|
||||
this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id),
|
||||
'Transitioned to the configuration index route.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module('Edit view', function () {
|
||||
module('isEnterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
test('it defaults to Azure accessType if Azure account fields are already set', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure');
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('azure')).isChecked('Azure accessType is checked');
|
||||
assert.dom(SES.wif.accessType('azure')).isDisabled('Azure accessType is disabled');
|
||||
assert.dom(SES.wif.accessType('wif')).isNotChecked('WIF accessType is not checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSubtext)
|
||||
.hasText('You cannot edit Access Type if you have already saved access credentials.');
|
||||
});
|
||||
|
||||
test('it defaults to WIF accessType if WIF fields are already set', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure-wif');
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert.dom(SES.wif.accessType('azure')).isNotChecked('azure accessType is not checked');
|
||||
assert.dom(SES.wif.accessType('azure')).isDisabled('azure accessType is disabled');
|
||||
assert.dom(GENERAL.inputByAttr('identityTokenAudience')).hasValue(this.config.identityTokenAudience);
|
||||
assert
|
||||
.dom(SES.wif.accessTypeSubtext)
|
||||
.hasText('You cannot edit Access Type if you have already saved access credentials.');
|
||||
assert.dom(GENERAL.ttl.input('Identity token TTL')).hasValue('2'); // 7200 on payload is 2hrs in ttl picker
|
||||
});
|
||||
|
||||
test('it renders issuer if global issuer is already set', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure-wif');
|
||||
this.issuerConfig = createConfig(this.store, this.id, 'issuer');
|
||||
this.issuerConfig.issuer = 'https://foo-bar-blah.com';
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked');
|
||||
assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('issuer'))
|
||||
.hasValue(
|
||||
this.issuerConfig.issuer,
|
||||
`it has the global issuer value of ${this.issuerConfig.issuer}`
|
||||
);
|
||||
});
|
||||
|
||||
test('it allows you to change accessType if record does not have wif or azure values already set', async function (assert) {
|
||||
// the model does not have to be new for a user to see the option to change the access type.
|
||||
// the access type is only disabled if the model has values already set for access type fields.
|
||||
this.config = createConfig(this.store, this.id, 'azure-generic');
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled');
|
||||
assert.dom(SES.wif.accessType('azure')).isNotDisabled('Azure accessType is NOT disabled');
|
||||
});
|
||||
|
||||
test('it shows previously saved config information', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure-generic');
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inputByAttr('subscriptionId')).hasValue(this.config.subscriptionId);
|
||||
assert.dom(GENERAL.inputByAttr('clientId')).hasValue(this.config.clientId);
|
||||
assert.dom(GENERAL.inputByAttr('tenantId')).hasValue(this.config.tenantId);
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('clientSecret'))
|
||||
.hasValue('**********', 'clientSecret is masked on edit the value');
|
||||
});
|
||||
|
||||
test('it requires a double click to change the client secret', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure');
|
||||
await this.renderComponent();
|
||||
|
||||
this.server.post(configUrl('azure', this.id), (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(
|
||||
payload.client_secret,
|
||||
'new-secret',
|
||||
'post request was made to azure/config with the updated client_secret.'
|
||||
);
|
||||
});
|
||||
|
||||
await click(GENERAL.enableField('clientSecret'));
|
||||
await click('[data-test-button="toggle-masked"]');
|
||||
await fillIn(GENERAL.inputByAttr('clientSecret'), 'new-secret');
|
||||
await click(GENERAL.saveButton);
|
||||
});
|
||||
});
|
||||
module('isCommunity', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.type = 'community';
|
||||
});
|
||||
|
||||
test('it does not show access types but defaults to Azure account fields', async function (assert) {
|
||||
this.config = createConfig(this.store, this.id, 'azure-generic');
|
||||
await this.renderComponent();
|
||||
assert.dom(SES.wif.accessTypeSection).doesNotExist('Access type section does not render');
|
||||
assert.dom(GENERAL.inputByAttr('clientId')).hasValue(this.config.clientId);
|
||||
assert.dom(GENERAL.inputByAttr('subscriptionId')).hasValue(this.config.subscriptionId);
|
||||
assert.dom(GENERAL.inputByAttr('tenantId')).hasValue(this.config.tenantId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
9
ui/types/vault/models/azure/config.d.ts
vendored
9
ui/types/vault/models/azure/config.d.ts
vendored
@@ -17,7 +17,14 @@ export default class AzureConfig extends Model {
|
||||
environment: string | undefined;
|
||||
rootPasswordTtl: string | undefined;
|
||||
|
||||
get attrs(): any;
|
||||
get displayAttrs(): any;
|
||||
get isWifPluginConfigured(): boolean;
|
||||
get isAzureAccountConfigured(): boolean;
|
||||
get fieldGroupsWif(): any;
|
||||
get fieldGroupsAzure(): any;
|
||||
formFieldGroups(accessType?: string): {
|
||||
[key: string]: string[];
|
||||
}[];
|
||||
changedAttributes(): {
|
||||
[key: string]: unknown[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user