/** * Copyright IBM Corp. 2016, 2025 * SPDX-License-Identifier: BUSL-1.1 */ import Component from '@glimmer/component'; import { task } from 'ember-concurrency'; import { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { convertToSeconds } from 'core/utils/duration-utils'; import { action } from '@ember/object'; import type Router from '@ember/routing/router'; import type FlashMessageService from 'vault/services/flash-messages'; import type ApiService from 'vault/services/api'; import type SecretsEngineResource from 'vault/resources/secrets/engine'; import type UnsavedChangesService from 'vault/services/unsaved-changes'; const CHARACTER_LIMIT = 500; /** * @module GeneralSettingsComponent is used to configure the SSH secret engine. * * @example * ```js * * ``` * * @param {string} secretsEngine - secrets engine resource * @param {string} versions - list of versions for a given secret engine */ interface Args { model: { secretsEngine: SecretsEngineResource; versions: string[]; }; } export default class GeneralSettingsComponent extends Component { @service declare readonly router: Router; @service declare readonly api: ApiService; @service declare readonly flashMessages: FlashMessageService; @service declare readonly unsavedChanges: UnsavedChangesService; @tracked errorMessage: string | string[] | null = null; @tracked errors: string[] = []; @tracked invalidFormAlert: string | null = null; @tracked showUnsavedChangesModal = false; @tracked defaultLeaseUnit = ''; @tracked maxLeaseUnit = ''; originalModel = JSON.parse(JSON.stringify(this.args.model)); get modalChangedFields() { const changedFieldsCopy = [...this.unsavedChanges.changedFields]; const configIndex = this.unsavedChanges.changedFields.indexOf('config'); if (configIndex === -1) return this.unsavedChanges.changedFields; changedFieldsCopy[configIndex] = 'Secrets duration'; return changedFieldsCopy; } validateTtl(ttlValue: FormDataEntryValue | number | null) { if (isNaN(Number(ttlValue))) { return false; } return true; } validateDescription(description: FormDataEntryValue | string) { return description?.toString().length <= CHARACTER_LIMIT; } validateForm() { const { defaultLeaseTime, maxLeaseTime, description } = this.formData; const errorMessages = []; if (!this.validateTtl(defaultLeaseTime) || !this.validateTtl(maxLeaseTime)) { errorMessages.push('TTL should only contain numbers.'); } if (description && !this.validateDescription(description)) { const charactersExceedBy = description?.toString().length - CHARACTER_LIMIT; errorMessages.push(`Engine description exceeds character limit by ${charactersExceedBy}.`); } this.errors = errorMessages; if (errorMessages.length) return false; return true; } hasTtlValueChanged(ttlTime: number, ttlUnit: string, ttlKey: 'max_lease_ttl' | 'default_lease_ttl') { const defaultLeaseInSecs = convertToSeconds(ttlTime, ttlUnit); if (defaultLeaseInSecs === this?.originalModel?.secretsEngine?.config[ttlKey]) { return false; } return true; } get formData() { const form = document.getElementById('general-settings-form'); const fd = new FormData(form as HTMLFormElement); const fdDefaultLeaseTime = Number(fd.get('default_lease_ttl-time')); const fdDefaultLeaseUnit = fd.get('default_lease_ttl-unit')?.toString() || 's'; const fdMaxLeaseTime = Number(fd.get('max_lease_ttl-time')); const fdMaxLeaseUnit = fd.get('max_lease_ttl-unit')?.toString() || 's'; return { defaultLeaseTime: fdDefaultLeaseTime, defaultLeaseUnit: fdDefaultLeaseUnit, maxLeaseTime: fdMaxLeaseTime, maxLeaseUnit: fdMaxLeaseUnit, description: fd.get('description'), version: fd.get('plugin-version'), }; } formatTuneParams() { const { defaultLeaseTime, defaultLeaseUnit, maxLeaseTime, maxLeaseUnit, description, version } = this.formData; const hasDefaultTtlValueChanged = this.hasTtlValueChanged( defaultLeaseTime, defaultLeaseUnit, 'default_lease_ttl' ); const hasMaxTtlValueChanged = this.hasTtlValueChanged(maxLeaseTime, maxLeaseUnit, 'max_lease_ttl'); const hasDescriptionChanged = description !== this?.originalModel?.secretsEngine?.description; const hasPluginVersionChanged = version && version !== this?.originalModel?.secretsEngine?.running_plugin_version; const defaultLeaseTtl = hasDefaultTtlValueChanged ? `${defaultLeaseTime}${defaultLeaseUnit}` : undefined; const maxLeaseTtl = hasMaxTtlValueChanged ? `${maxLeaseTime}${maxLeaseUnit}` : undefined; const pluginVersion = hasPluginVersionChanged ? version : undefined; const pluginDescription = hasDescriptionChanged ? description : undefined; return { defaultLeaseTtl, maxLeaseTtl, pluginVersion, pluginDescription, }; } saveGeneralSettings = task(async (event?) => { // event is an optional arg because saveGeneralSettings can be called in the closeAndHandle function. // There are instances where we will save in the modal where that doesn't require an event. if (event) event.preventDefault(); if (!this.validateForm()) return; try { const { defaultLeaseTtl, maxLeaseTtl, pluginVersion, pluginDescription } = this.formatTuneParams(); await this.api.sys.mountsTuneConfigurationParameters(this.args?.model?.secretsEngine?.id, { description: pluginDescription as string | undefined, default_lease_ttl: defaultLeaseTtl, max_lease_ttl: maxLeaseTtl, plugin_version: pluginVersion as string | undefined, }); this.flashMessages.success('Engine settings successfully updated.', { title: 'Configuration saved' }); this.unsavedChanges.showModal = false; this.router.transitionTo(this.router.currentRouteName); } catch (e) { const { message } = await this.api.parseError(e); this.errorMessage = message; } }); @action discardChanges() { const currentRouteName = this.router.currentRouteName; this.router.transitionTo(currentRouteName); } }