diff --git a/ui/app/adapters/sync/association.js b/ui/app/adapters/sync/association.js deleted file mode 100644 index 116930ce81..0000000000 --- a/ui/app/adapters/sync/association.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ApplicationAdapter from 'vault/adapters/application'; -import { assert } from '@ember/debug'; -import { all } from 'rsvp'; - -export default class SyncAssociationAdapter extends ApplicationAdapter { - namespace = 'v1/sys/sync'; - - buildURL(modelName, id, snapshot, requestType, query = {}) { - const { destinationType, destinationName } = snapshot ? snapshot.attributes() : query; - if (!destinationType || !destinationName) { - return `${super.buildURL()}/associations`; - } - const { action } = snapshot?.adapterOptions || {}; - const uri = action ? `/${action}` : ''; - return `${super.buildURL()}/destinations/${destinationType}/${destinationName}/associations${uri}`; - } - - query(store, { modelName }, query) { - // endpoint doesn't accept the typical list query param and we don't want to pass options from lazyPaginatedQuery - const url = this.buildURL(modelName, null, null, 'query', query); - return this.ajax(url, 'GET'); - } - - // typically associations are queried for a specific destination which is what the standard query method does - // in specific cases we can query all associations to access total_associations and total_secrets values - queryAll() { - const url = `${this.buildURL('sync/association')}`; - return this.ajax(url, 'GET', { data: { list: true } }).then((response) => { - const { total_associations, total_secrets } = response.data; - return { total_associations, total_secrets }; - }); - } - - // fetch associations for many destinations - // returns aggregated association information for each destination - // information includes total associations, total unsynced and most recent updated datetime - async fetchByDestinations(destinations) { - const promises = destinations.map(({ name: destinationName, type: destinationType }) => { - return this.query(this.store, { modelName: 'sync/association' }, { destinationName, destinationType }); - }); - const queryResponses = await all(promises); - const serializer = this.store.serializerFor('sync/association'); - return queryResponses.map((response) => serializer.normalizeFetchByDestinations(response)); - } - - // array of association data for each destination a secret is synced to - fetchSyncStatus({ mount, secretName }) { - const url = `${this.buildURL()}/destinations`; - return this.ajax(url, 'GET', { data: { mount, secret_name: secretName } }).then((resp) => { - const { associated_destinations } = resp.data; - const syncData = []; - for (const key in associated_destinations) { - const data = associated_destinations[key]; - // renaming keys to match query() response - syncData.push({ - destinationType: data.type, - destinationName: data.name, - syncStatus: data.sync_status, - updatedAt: data.updated_at, - }); - } - return syncData; - }); - } - - // snapshot is needed for mount and secret_name values which are used to parse response since all associations are returned - _setOrRemove(store, { modelName }, snapshot) { - assert( - "action type of set or remove required when saving association => association.save({ adapterOptions: { action: 'set' }})", - ['set', 'remove'].includes(snapshot?.adapterOptions?.action) - ); - const url = this.buildURL(modelName, null, snapshot); - const data = snapshot.serialize(); - const serializer = store.serializerFor('sync/association'); - - return this.ajax(url, 'POST', { data }).then((resp) => { - const association = Object.values(resp.data.associated_secrets).find((association) => { - return association.mount === data.mount && association.secret_name === data.secret_name; - }); - - // generate an id if an association is found - // (an association may not be found if the secret is being unsynced) - const id = association ? serializer.generateId(association) : undefined; - - return { - ...association, - id, - destinationName: resp.data.store_name, - destinationType: resp.data.store_type, - }; - }); - } - - createRecord() { - return this._setOrRemove(...arguments); - } - - updateRecord() { - return this._setOrRemove(...arguments); - } -} diff --git a/ui/app/adapters/sync/destination.js b/ui/app/adapters/sync/destination.js deleted file mode 100644 index ef4e4e7f75..0000000000 --- a/ui/app/adapters/sync/destination.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ApplicationAdapter from 'vault/adapters/application'; -import { pluralize } from 'ember-inflector'; - -export default class SyncDestinationAdapter extends ApplicationAdapter { - namespace = 'v1/sys'; - - pathForType(modelName) { - return modelName === 'sync/destination' ? pluralize(modelName) : modelName; - } - - urlForCreateRecord(modelName, snapshot) { - const { name } = snapshot.attributes(); - return `${super.urlForCreateRecord(modelName, snapshot)}/${name}`; - } - - updateRecord(store, { modelName }, snapshot) { - const { name } = snapshot.attributes(); - return this.ajax(`${this.buildURL(modelName)}/${name}`, 'PATCH', { data: snapshot.serialize() }); - } - - urlForDeleteRecord(id, modelName, snapshot) { - const { name, type } = snapshot.attributes(); - // the only delete option in the UI is to purge which unsyncs all secrets prior to deleting - return `${this.buildURL('sync/destinations')}/${type}/${name}?purge=true`; - } - - query(store, { modelName }) { - return this.ajax(this.buildURL(modelName), 'GET', { data: { list: true } }); - } - - createRecord(store, type, snapshot) { - const id = `${snapshot.record.type}:${snapshot.record.name}`; - return super.createRecord(...arguments).then((resp) => { - resp.id = id; - return resp; - }); - } - - // return normalized query response - // useful for fetching data directly without loading models into store - async normalizedQuery() { - const queryResponse = await this.query(this.store, { modelName: 'sync/destination' }); - const serializer = this.store.serializerFor('sync/destination'); - return serializer.extractLazyPaginatedData(queryResponse); - } -} diff --git a/ui/app/adapters/sync/destinations/aws-sm.js b/ui/app/adapters/sync/destinations/aws-sm.js deleted file mode 100644 index 6984f995bc..0000000000 --- a/ui/app/adapters/sync/destinations/aws-sm.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationAdapter from '../destination'; - -export default class SyncDestinationsAwsSecretsManagerAdapter extends SyncDestinationAdapter {} diff --git a/ui/app/adapters/sync/destinations/azure-kv.js b/ui/app/adapters/sync/destinations/azure-kv.js deleted file mode 100644 index a35ea29a69..0000000000 --- a/ui/app/adapters/sync/destinations/azure-kv.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationAdapter from '../destination'; - -export default class SyncDestinationsAzureKeyVaultAdapter extends SyncDestinationAdapter {} diff --git a/ui/app/adapters/sync/destinations/gcp-sm.js b/ui/app/adapters/sync/destinations/gcp-sm.js deleted file mode 100644 index 6fa4035610..0000000000 --- a/ui/app/adapters/sync/destinations/gcp-sm.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationAdapter from '../destination'; - -export default class SyncDestinationGoogleCloudSecretManagerAdapter extends SyncDestinationAdapter {} diff --git a/ui/app/adapters/sync/destinations/gh.js b/ui/app/adapters/sync/destinations/gh.js deleted file mode 100644 index e42488c7e8..0000000000 --- a/ui/app/adapters/sync/destinations/gh.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationAdapter from '../destination'; - -export default class SyncDestinationsGithubAdapter extends SyncDestinationAdapter {} diff --git a/ui/app/adapters/sync/destinations/vercel-project.js b/ui/app/adapters/sync/destinations/vercel-project.js deleted file mode 100644 index f0062d55c5..0000000000 --- a/ui/app/adapters/sync/destinations/vercel-project.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationAdapter from '../destination'; - -export default class SyncDestinationsVercelProjectAdapter extends SyncDestinationAdapter {} diff --git a/ui/app/models/sync/association.js b/ui/app/models/sync/association.js deleted file mode 100644 index 0803753bb2..0000000000 --- a/ui/app/models/sync/association.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Model, { attr } from '@ember-data/model'; -import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; - -export default class SyncAssociationModel extends Model { - @attr mount; - @attr secretName; - @attr syncStatus; - @attr updatedAt; - // destination related properties that are not serialized to payload - @attr destinationName; - @attr destinationType; - @attr subKey; // this property is added if a destination has 'secret-key' granularity - - @lazyCapabilities( - apiPath`sys/sync/destinations/${'destinationType'}/${'destinationName'}/associations/set`, - 'destinationType', - 'destinationName' - ) - setAssociationPath; - - @lazyCapabilities( - apiPath`sys/sync/destinations/${'destinationType'}/${'destinationName'}/associations/remove`, - 'destinationType', - 'destinationName' - ) - removeAssociationPath; - - get canSync() { - return this.setAssociationPath.get('canUpdate') !== false; - } - - get canUnsync() { - return this.removeAssociationPath.get('canUpdate') !== false; - } -} diff --git a/ui/app/models/sync/destination.js b/ui/app/models/sync/destination.js deleted file mode 100644 index 858c5a719a..0000000000 --- a/ui/app/models/sync/destination.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Model, { attr } from '@ember-data/model'; -import { findDestination } from 'vault/helpers/sync-destinations'; -import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; -import { withModelValidations } from 'vault/decorators/model-validations'; - -// Base model for all secret sync destination types -const validations = { - name: [ - { type: 'presence', message: 'Name is required.' }, - { type: 'containsWhiteSpace', message: 'Name cannot contain whitespace.' }, - ], -}; - -@withModelValidations(validations) -export default class SyncDestinationModel extends Model { - @attr('string', { subText: 'Specifies the name for this destination.', editDisabled: true }) - name; - - @attr type; - - @attr('string', { - subText: - 'Go-template string that indicates how to format the secret name at the destination. The default template varies by destination type but is generally in the form of "vault-{{ .MountAccessor }}-{{ .SecretPath }}" e.g. "vault-kv_9a8f68ad-my-secret-1". Optional.', - }) - secretNameTemplate; - - @attr('string', { - editType: 'radio', - label: 'Secret sync granularity', - possibleValues: [ - { - label: 'Secret path', - subText: 'Sync entire secret contents as a single entry at the destination.', - value: 'secret-path', - }, - { - label: 'Secret key', - subText: 'Sync each key-value pair of secret data as a distinct entry at the destination.', - helpText: - 'Only top-level keys will be synced and any nested or complex values will be encoded as a JSON string.', - value: 'secret-key', - }, - ], - }) - granularity; // default value depends on type and is set in create route - - // only present if delete action has been initiated - @attr('string') purgeInitiatedAt; - @attr('string') purgeError; - - // findDestination returns static attributes for each destination type - get icon() { - return findDestination(this.type)?.icon; - } - - get typeDisplayName() { - return findDestination(this.type)?.name; - } - - get maskedParams() { - return findDestination(this.type)?.maskedParams; - } - - @lazyCapabilities(apiPath`sys/sync/destinations/${'type'}/${'name'}`, 'type', 'name') destinationPath; - @lazyCapabilities(apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/set`, 'type', 'name') - setAssociationPath; - - get canCreate() { - return this.destinationPath.get('canCreate') !== false; - } - get canDelete() { - return this.destinationPath.get('canDelete') !== false; - } - get canEdit() { - return this.destinationPath.get('canUpdate') !== false && !this.purgeInitiatedAt; - } - get canRead() { - return this.destinationPath.get('canRead') !== false; - } - get canSync() { - return this.setAssociationPath.get('canUpdate') !== false && !this.purgeInitiatedAt; - } -} diff --git a/ui/app/models/sync/destinations/aws-sm.js b/ui/app/models/sync/destinations/aws-sm.js deleted file mode 100644 index 27cfa68556..0000000000 --- a/ui/app/models/sync/destinations/aws-sm.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationModel from '../destination'; -import { attr } from '@ember-data/model'; -import { withFormFields } from 'vault/decorators/model-form-fields'; - -// displayFields are used on the destination details view -const displayFields = [ - // connection details - 'name', - 'region', - 'accessKeyId', - 'secretAccessKey', - 'roleArn', - 'externalId', - // sync config options - 'granularity', - 'secretNameTemplate', - 'customTags', -]; -// formFieldGroups are used on the create-edit destination view -const formFieldGroups = [ - { - default: ['name', 'region', 'roleArn', 'externalId'], - }, - { Credentials: ['accessKeyId', 'secretAccessKey'] }, - { 'Advanced configuration': ['granularity', 'secretNameTemplate', 'customTags'] }, -]; -@withFormFields(displayFields, formFieldGroups) -export default class SyncDestinationsAwsSecretsManagerModel extends SyncDestinationModel { - @attr('string', { - label: 'Access key ID', - subText: - 'Access key ID to authenticate against the secrets manager. If empty, Vault will use the AWS_ACCESS_KEY_ID environment variable if configured.', - sensitive: true, - noCopy: true, - }) - accessKeyId; // obfuscated, never returned by API - - @attr('string', { - label: 'Secret access key', - subText: - 'Secret access key to authenticate against the secrets manager. If empty, Vault will use the AWS_SECRET_ACCESS_KEY environment variable if configured.', - sensitive: true, - noCopy: true, - }) - secretAccessKey; // obfuscated, never returned by API - - @attr('string', { - subText: - 'For AWS secrets manager, the name of the region must be supplied, something like “us-west-1.” If empty, Vault will use the AWS_REGION environment variable if configured.', - editDisabled: true, - }) - region; - - @attr('object', { - subText: - 'An optional set of informational key-value pairs added as additional metadata on secrets synced to this destination. Custom tags are merged with built-in tags.', - editType: 'kv', - }) - customTags; - - @attr('string', { - label: 'Role ARN', - subText: - 'Specifies a role to assume when connecting to AWS. When assuming a role, Vault uses temporary STS credentials to authenticate.', - }) - roleArn; - - @attr('string', { - label: 'External ID', - subText: - 'Optional extra protection that must match the trust policy granting access to the AWS IAM role ARN. We recommend using a different random UUID per destination.', - }) - externalId; -} diff --git a/ui/app/models/sync/destinations/azure-kv.js b/ui/app/models/sync/destinations/azure-kv.js deleted file mode 100644 index 8290c0824d..0000000000 --- a/ui/app/models/sync/destinations/azure-kv.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationModel from '../destination'; -import { attr } from '@ember-data/model'; -import { withFormFields } from 'vault/decorators/model-form-fields'; -// displayFields are used on the destination details view -const displayFields = [ - // connection details - 'name', - 'keyVaultUri', - 'tenantId', - 'cloud', - 'clientId', - 'clientSecret', - // vault sync config options - 'granularity', - 'secretNameTemplate', - 'customTags', -]; -// formFieldGroups are used on the create-edit destination view -const formFieldGroups = [ - { - default: ['name', 'keyVaultUri', 'tenantId', 'cloud', 'clientId'], - }, - { Credentials: ['clientSecret'] }, - { 'Advanced configuration': ['granularity', 'secretNameTemplate', 'customTags'] }, -]; -@withFormFields(displayFields, formFieldGroups) -export default class SyncDestinationsAzureKeyVaultModel extends SyncDestinationModel { - @attr('string', { - label: 'Key Vault URI', - subText: - 'URI of an existing Azure Key Vault instance. If empty, Vault will use the KEY_VAULT_URI environment variable if configured.', - editDisabled: true, - }) - keyVaultUri; - - @attr('string', { - label: 'Client ID', - subText: - 'Client ID of an Azure app registration. If empty, Vault will use the AZURE_CLIENT_ID environment variable if configured.', - }) - clientId; - - @attr('string', { - subText: - 'Client secret of an Azure app registration. If empty, Vault will use the AZURE_CLIENT_SECRET environment variable if configured.', - sensitive: true, - noCopy: true, - }) - clientSecret; // obfuscated, never returned by API - - @attr('string', { - label: 'Tenant ID', - subText: - 'ID of the target Azure tenant. If empty, Vault will use the AZURE_TENANT_ID environment variable if configured.', - editDisabled: true, - }) - tenantId; - - @attr('string', { - subText: 'Specifies a cloud for the client. The default is Azure Public Cloud.', - editDisabled: true, - }) - cloud; - - @attr('object', { - subText: - 'An optional set of informational key-value pairs added as additional metadata on secrets synced to this destination. Custom tags are merged with built-in tags.', - editType: 'kv', - }) - customTags; -} diff --git a/ui/app/models/sync/destinations/gcp-sm.js b/ui/app/models/sync/destinations/gcp-sm.js deleted file mode 100644 index 09b4025a45..0000000000 --- a/ui/app/models/sync/destinations/gcp-sm.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationModel from '../destination'; -import { attr } from '@ember-data/model'; -import { withFormFields } from 'vault/decorators/model-form-fields'; -// displayFields are used on the destination details view -const displayFields = [ - // connection details - 'name', - 'projectId', - 'credentials', - // vault sync config options - 'granularity', - 'secretNameTemplate', - 'customTags', -]; -// formFieldGroups are used on the create-edit destination view -const formFieldGroups = [ - { default: ['name', 'projectId'] }, - { Credentials: ['credentials'] }, - { 'Advanced configuration': ['granularity', 'secretNameTemplate', 'customTags'] }, -]; -@withFormFields(displayFields, formFieldGroups) -export default class SyncDestinationsGoogleCloudSecretManagerModel extends SyncDestinationModel { - @attr('string', { - label: 'Project ID', - subText: - 'The target project to manage secrets in. If set, overrides the project derived from the service account JSON credentials or application default credentials.', - }) - projectId; - - @attr('string', { - label: 'JSON credentials', - subText: - 'If empty, Vault will use the GOOGLE_APPLICATION_CREDENTIALS environment variable if configured.', - editType: 'file', - docLink: '/vault/docs/secrets/gcp#authentication', - }) - credentials; // obfuscated, never returned by API. Masking handled by EnableInput component - - @attr('object', { - subText: - 'An optional set of informational key-value pairs added as additional metadata on secrets synced to this destination. Custom tags are merged with built-in tags.', - editType: 'kv', - }) - customTags; -} diff --git a/ui/app/models/sync/destinations/gh.js b/ui/app/models/sync/destinations/gh.js deleted file mode 100644 index d5a5640303..0000000000 --- a/ui/app/models/sync/destinations/gh.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationModel from '../destination'; -import { attr } from '@ember-data/model'; -import { withFormFields } from 'vault/decorators/model-form-fields'; -// displayFields are used on the destination details view -const displayFields = [ - // connection details - 'name', - 'repositoryOwner', - 'repositoryName', - 'accessToken', - // vault sync config options - 'granularity', - 'secretNameTemplate', -]; - -// formFieldGroups are used on the create-edit destination view -const formFieldGroups = [ - { default: ['name', 'repositoryOwner', 'repositoryName'] }, - { Credentials: ['accessToken'] }, - { 'Advanced configuration': ['granularity', 'secretNameTemplate'] }, -]; - -@withFormFields(displayFields, formFieldGroups) -export default class SyncDestinationsGithubModel extends SyncDestinationModel { - @attr('string', { - subText: - 'Personal access token to authenticate to the GitHub repository. If empty, Vault will use the GITHUB_ACCESS_TOKEN environment variable if configured.', - sensitive: true, - noCopy: true, - }) - accessToken; // obfuscated, never returned by API - - @attr('string', { - subText: - 'Github organization or username that owns the repository. If empty, Vault will use the GITHUB_REPOSITORY_OWNER environment variable if configured.', - editDisabled: true, - }) - repositoryOwner; - - @attr('string', { - subText: - 'The name of the Github repository to connect to. If empty, Vault will use the GITHUB_REPOSITORY_NAME environment variable if configured.', - editDisabled: true, - }) - repositoryName; -} diff --git a/ui/app/models/sync/destinations/vercel-project.js b/ui/app/models/sync/destinations/vercel-project.js deleted file mode 100644 index 1454d8e04d..0000000000 --- a/ui/app/models/sync/destinations/vercel-project.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationModel from '../destination'; -import { attr } from '@ember-data/model'; -import { withFormFields } from 'vault/decorators/model-form-fields'; -import { withModelValidations } from 'vault/decorators/model-validations'; - -const validations = { - name: [{ type: 'presence', message: 'Name is required.' }], - teamId: [ - { - validator: (model) => - !model.isNew && Object.keys(model.changedAttributes()).includes('teamId') ? false : true, - message: 'Team ID should only be updated if the project was transferred to another account.', - level: 'warn', - }, - ], - // getter/setter for the deploymentEnvironments model attribute - deploymentEnvironmentsArray: [{ type: 'presence', message: 'At least one environment is required.' }], -}; -// displayFields are used on the destination details view -const displayFields = [ - // connection details - 'name', - 'accessToken', - 'projectId', - 'teamId', - 'deploymentEnvironments', - // vault sync config options - 'granularity', - 'secretNameTemplate', -]; -// formFieldGroups are used on the create-edit destination view -const formFieldGroups = [ - { default: ['name', 'projectId', 'teamId', 'deploymentEnvironments'] }, - { Credentials: ['accessToken'] }, - { 'Advanced configuration': ['granularity', 'secretNameTemplate'] }, -]; -@withModelValidations(validations) -@withFormFields(displayFields, formFieldGroups) -export default class SyncDestinationsVercelProjectModel extends SyncDestinationModel { - @attr('string', { - subText: 'Vercel API access token with the permissions to manage environment variables.', - sensitive: true, - noCopy: true, - }) - accessToken; // obfuscated, never returned by API - - @attr('string', { - label: 'Project ID', - subText: 'Project ID where to manage environment variables.', - editDisabled: true, - }) - projectId; - - @attr('string', { - label: 'Team ID', - subText: 'Team ID the project belongs to. Optional.', - }) - teamId; - - // commaString transforms param from the server's array type - // to a comma string so changedAttributes() will track changes - @attr('commaString', { - subText: 'Deployment environments where the environment variables are available.', - editType: 'checkboxList', - possibleValues: ['development', 'preview', 'production'], - fieldValue: 'deploymentEnvironmentsArray', // getter/setter used to update value - }) - deploymentEnvironments; - - // Arrays are easier for managing multi-option selection - // these get/set the deploymentEnvironments attribute via arrays - get deploymentEnvironmentsArray() { - // if undefined or an empty string, return empty array - return !this.deploymentEnvironments ? [] : this.deploymentEnvironments.split(','); - } - - set deploymentEnvironmentsArray(value) { - this.deploymentEnvironments = value.join(','); - } -} diff --git a/ui/app/serializers/sync/association.js b/ui/app/serializers/sync/association.js deleted file mode 100644 index 7fb8f13c2b..0000000000 --- a/ui/app/serializers/sync/association.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ApplicationSerializer from 'vault/serializers/application'; -import { findDestination } from 'core/helpers/sync-destinations'; - -export default class SyncAssociationSerializer extends ApplicationSerializer { - attrs = { - destinationName: { serialize: false }, - destinationType: { serialize: false }, - syncStatus: { serialize: false }, - updatedAt: { serialize: false }, - subKey: { serialize: false }, - }; - - generateId(data) { - let id = `${data.mount}/${data.secret_name}`; - if (data.sub_key) { - id += `/${data.sub_key}`; - } - return id; - } - - extractLazyPaginatedData(payload) { - if (payload) { - const { store_name, store_type, associated_secrets } = payload.data; - const secrets = []; - for (const key in associated_secrets) { - const data = associated_secrets[key]; - data.id = this.generateId(data); - const association = { - destinationName: store_name, - destinationType: store_type, - ...data, - }; - secrets.push(association); - } - return secrets; - } - return payload; - } - - normalizeFetchByDestinations(payload) { - const { store_name, store_type, associated_secrets } = payload.data; - const unsynced = []; - let lastUpdated; - - for (const key in associated_secrets) { - const association = associated_secrets[key]; - // for display purposes, any status other than SYNCED is considered unsynced - if (association.sync_status !== 'SYNCED') { - unsynced.push(association.sync_status); - } - // use the most recent updated_at value as the last synced date - const updated = new Date(association.updated_at); - if (!lastUpdated || updated > lastUpdated) { - lastUpdated = updated; - } - } - - const associationCount = Object.entries(associated_secrets).length; - return { - icon: findDestination(store_type).icon, - name: store_name, - type: store_type, - associationCount, - status: associationCount ? (unsynced.length ? `${unsynced.length} Unsynced` : 'All synced') : null, - lastUpdated, - }; - } -} diff --git a/ui/app/serializers/sync/destination.js b/ui/app/serializers/sync/destination.js deleted file mode 100644 index 6d6fe9020d..0000000000 --- a/ui/app/serializers/sync/destination.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ApplicationSerializer from 'vault/serializers/application'; -import { decamelize } from '@ember/string'; -export default class SyncDestinationSerializer extends ApplicationSerializer { - attrs = { - name: { serialize: false }, - type: { serialize: false }, - purgeInitiatedAt: { serialize: false }, - purgeError: { serialize: false }, - }; - - serialize(snapshot) { - const data = super.serialize(snapshot); - if (snapshot.isNew) return data; - - // only send changed parameters for PATCH requests - const changedKeys = Object.keys(snapshot.changedAttributes()).map((key) => decamelize(key)); - return changedKeys.reduce((payload, key) => { - if (changedKeys.includes('custom_tags')) { - const [oldObject, newObject] = snapshot.changedAttributes()['customTags']; - if (oldObject && newObject) { - // manually compare the new and old keys of custom_tags object to determine which need to be removed - const oldKeys = Object.keys(oldObject).filter((k) => !Object.keys(newObject).includes(k)); - // add tags_to_remove to the payload if there is a diff - if (oldKeys.length > 0) payload.tags_to_remove = oldKeys; - } - } - payload[key] = data[key]; - return payload; - }, {}); - } - - // interrupt application's normalizeItems, which is called in normalizeResponse by application serializer - normalizeResponse(store, primaryModelClass, payload, id, requestType) { - const transformedPayload = this._normalizePayload(payload, requestType); - return super.normalizeResponse(store, primaryModelClass, transformedPayload, id, requestType); - } - - extractLazyPaginatedData(payload) { - const transformedPayload = []; - // loop through each destination type (keys in key_info) - for (const key in payload.data.key_info) { - // iterate through each type's destination names - payload.data.key_info[key].forEach((name) => { - // remove trailing slash from key - const type = key.replace(/\/$/, ''); - const id = `${type}/${name}`; - // create object with destination's id and attributes, add to payload - transformedPayload.push({ id, name, type }); - }); - } - return transformedPayload; - } - - _normalizePayload(payload, requestType) { - // if request is from lazyPaginatedQuery it will already have been extracted and meta will be set on object - // for store.query it will be the raw response which will need to be extracted - if (requestType === 'query') { - return payload.meta ? payload : this.extractLazyPaginatedData(payload); - } else if (payload?.data) { - // uses name for id and spreads connection_details object into data - const { data } = payload; - const { connection_details, options } = data; - data.id = data.name; - delete data.connection_details; - delete data.options; - // granularity keys differ from payload to response -- normalize to payload format - if (options) { - options.granularity = options.granularity_level; - delete options.granularity_level; - } - return { data: { ...data, ...connection_details, ...options } }; - } - return payload; - } -} diff --git a/ui/app/serializers/sync/destinations/aws-sm.js b/ui/app/serializers/sync/destinations/aws-sm.js deleted file mode 100644 index 4155dce204..0000000000 --- a/ui/app/serializers/sync/destinations/aws-sm.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationSerializer from '../destination'; - -export default class SyncDestinationsAwsSecretsManagerSerializer extends SyncDestinationSerializer {} diff --git a/ui/app/serializers/sync/destinations/azure-kv.js b/ui/app/serializers/sync/destinations/azure-kv.js deleted file mode 100644 index e573e00db3..0000000000 --- a/ui/app/serializers/sync/destinations/azure-kv.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationSerializer from '../destination'; - -export default class SyncDestinationsAzureKeyVaultSerializer extends SyncDestinationSerializer {} diff --git a/ui/app/serializers/sync/destinations/gcp-sm.js b/ui/app/serializers/sync/destinations/gcp-sm.js deleted file mode 100644 index ae297171d5..0000000000 --- a/ui/app/serializers/sync/destinations/gcp-sm.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationSerializer from '../destination'; - -export default class SyncDestinationGoogleCloudSecretManagerSerializer extends SyncDestinationSerializer {} diff --git a/ui/app/serializers/sync/destinations/gh.js b/ui/app/serializers/sync/destinations/gh.js deleted file mode 100644 index 8ed542a33f..0000000000 --- a/ui/app/serializers/sync/destinations/gh.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationSerializer from '../destination'; - -export default class SyncDestinationsGithubSerializer extends SyncDestinationSerializer {} diff --git a/ui/app/serializers/sync/destinations/vercel-project.js b/ui/app/serializers/sync/destinations/vercel-project.js deleted file mode 100644 index 5e8f7b6545..0000000000 --- a/ui/app/serializers/sync/destinations/vercel-project.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import SyncDestinationSerializer from '../destination'; - -export default class SyncDestinationsVercelProjectSerializer extends SyncDestinationSerializer {} diff --git a/ui/tests/unit/adapters/sync/associations-test.js b/ui/tests/unit/adapters/sync/associations-test.js deleted file mode 100644 index 7bcb25129a..0000000000 --- a/ui/tests/unit/adapters/sync/associations-test.js +++ /dev/null @@ -1,190 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { setupTest } from 'ember-qunit'; -import { module, test } from 'qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { associationsResponse } from 'vault/mirage/handlers/sync'; -import sinon from 'sinon'; - -module('Unit | Adapter | sync | association', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - this.pagination = this.owner.lookup('service:pagination'); - - this.params = [ - { type: 'aws-sm', name: 'us-west-1' }, - { type: 'gh', name: 'baz' }, - ]; - this.params.forEach((params) => { - this.server.create('sync-destination', params.type, { name: params.name }); - const association = this.server.create('sync-association', { - ...params, - mount: 'foo', - secret_name: 'bar', - }); - if (!this.association) { - this.association = association; - } - }); - - this.newModel = this.store.createRecord('sync/association', { - destinationType: 'aws-sm', - destinationName: 'us-west-1', - mount: 'foo', - secretName: 'bar', - }); - }); - - test('it should make request to correct endpoint when querying', async function (assert) { - assert.expect(2); - - this.server.get('/sys/sync/destinations/:type/:name/associations', (schema, req) => { - // list query param not required for this endpoint - assert.deepEqual(req.queryParams, {}, 'query params stripped from request'); - assert.deepEqual(req.params, this.params[0], 'request is made to correct endpoint when querying'); - return associationsResponse(schema, req); - }); - - await this.pagination.lazyPaginatedQuery('sync/association', { - responsePath: 'data.keys', - page: 1, - destinationType: 'aws-sm', - destinationName: 'us-west-1', - }); - }); - - test('it should make request to correct endpoint for queryAll associations', async function (assert) { - assert.expect(3); - - this.server.get('/sys/sync/associations', (schema, req) => { - assert.ok(true, 'request is made to correct endpoint for queryAll'); - assert.propEqual(req.queryParams, { list: 'true' }, 'query params include list: true'); - return { - data: { - key_info: {}, - keys: [], - total_associations: 5, - total_secrets: 7, - }, - }; - }); - - const response = await this.store.adapterFor('sync/association').queryAll(); - const expected = { total_associations: 5, total_secrets: 7 }; - assert.deepEqual(response, expected, 'It returns correct values for queryAll'); - }); - - test('it should make request to correct endpoint when creating record', async function (assert) { - assert.expect(2); - - this.server.post('/sys/sync/destinations/:type/:name/associations/set', (schema, req) => { - assert.deepEqual(req.params, this.params[0], 'request is made to correct endpoint when querying'); - assert.deepEqual( - JSON.parse(req.requestBody), - { mount: 'foo', secret_name: 'bar' }, - 'Correct payload is sent when creating association' - ); - return associationsResponse(schema, req); - }); - - await this.newModel.save({ adapterOptions: { action: 'set' } }); - }); - - test('it should make request to correct endpoint when updating record', async function (assert) { - assert.expect(2); - - this.server.post('/sys/sync/destinations/:type/:name/associations/remove', (schema, req) => { - assert.deepEqual(req.params, this.params[0], 'request is made to correct endpoint when querying'); - assert.deepEqual( - JSON.parse(req.requestBody), - { mount: 'foo', secret_name: 'bar' }, - 'Correct payload is sent when removing association' - ); - return associationsResponse(schema, req); - }); - - this.store.pushPayload('sync/association', { - modelName: 'sync/association', - destinationType: 'aws-sm', - destinationName: 'us-west-1', - mount: 'foo', - secret_name: 'bar', - sync_status: 'SYNCED', - id: 'foo/bar', - }); - const model = this.store.peekRecord('sync/association', 'foo/bar'); - - await model.save({ adapterOptions: { action: 'remove' } }); - }); - - test('it should parse response from set/remove request', async function (assert) { - this.server.post('/sys/sync/destinations/:type/:name/associations/set', associationsResponse); - - const adapter = this.store.adapterFor('sync/association'); - // mock snapshot - const snapshot = { - attributes() { - return { destinationName: 'us-west-1', destinationType: 'aws-sm' }; - }, - serialize() { - return { mount: 'foo', secret_name: 'bar' }; - }, - adapterOptions: { action: 'set' }, - }; - const response = await adapter._setOrRemove(this.store, { modelName: 'sync/association' }, snapshot); - const { accessor, mount, secret_name, sync_status, name, type, updated_at } = this.association; - const expected = { - id: 'foo/bar', - accessor, - mount, - secret_name, - sync_status, - updated_at, - destinationType: type, - destinationName: name, - }; - - assert.deepEqual( - response, - expected, - 'Custom create/update record method makes request and parses response' - ); - }); - - test('it should throw error if save action is not passed in adapterOptions', async function (assert) { - assert.expect(1); - - try { - await this.newModel.save(); - } catch (e) { - assert.strictEqual( - e.message, - "Assertion Failed: action type of set or remove required when saving association => association.save({ adapterOptions: { action: 'set' }})" - ); - } - }); - - test('it should fetch and normalize many associations for fetchByDestinations', async function (assert) { - assert.expect(3); - - const handler = this.server.get('/sys/sync/destinations/:type/:name/associations', (schema, req) => { - // list query param not required for this endpoint - assert.deepEqual( - req.params, - this.params[handler.numberOfCalls - 1], - 'request is made to correct endpoint when querying' - ); - return associationsResponse(schema, req); - }); - - const spy = sinon.spy(this.store.serializerFor('sync/association'), 'normalizeFetchByDestinations'); - await this.store.adapterFor('sync/association').fetchByDestinations(this.params); - assert.true(spy.calledTwice, 'Serializer method used on each response'); - }); -}); diff --git a/ui/tests/unit/adapters/sync/destinations-test.js b/ui/tests/unit/adapters/sync/destinations-test.js deleted file mode 100644 index 10cea3164b..0000000000 --- a/ui/tests/unit/adapters/sync/destinations-test.js +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { setupTest } from 'ember-qunit'; -import { module, test } from 'qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { destinationTypes } from 'vault/helpers/sync-destinations'; -import sinon from 'sinon'; - -module('Unit | Adapter | sync | destination', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - }); - - test('it calls the correct endpoint for createRecord', async function (assert) { - const types = destinationTypes(); - assert.expect(types.length); - - for (const type of types) { - const name = 'my-dest'; - this.server.post(`sys/sync/destinations/${type}/${name}`, () => { - assert.ok(true, `request is made to POST sys/sync/destinations/${type}/my-dest endpoint on create`); - return { - data: { - connection_details: {}, - name, - type, - }, - }; - }); - this.model = this.store.createRecord(`sync/destinations/${type}`, { type, name }); - this.model.save(); - } - }); - - test('it calls the correct endpoint for updateRecord', async function (assert) { - const types = destinationTypes(); - assert.expect(types.length); - - for (const type of types) { - const data = this.server.create('sync-destination', type); - this.server.patch(`sys/sync/destinations/${type}/${data.name}`, () => { - assert.ok( - true, - `request is made to PATCH sys/sync/destinations/${type}/${data.name} endpoint on update` - ); - return { - data: {}, - }; - }); - const id = `${type}/${data.name}`; - data.id = id; - this.store.pushPayload(`sync/destinations/${type}`, { - modelName: `sync/destinations/${type}`, - ...data, - }); - this.model = this.store.peekRecord(`sync/destinations/${type}`, id); - this.model.save(); - } - }); - - test('it calls the correct endpoint for findRecord', async function (assert) { - const types = destinationTypes(); - assert.expect(types.length); - - for (const type of types) { - const name = 'my-dest'; - this.server.get(`sys/sync/destinations/${type}/${name}`, () => { - assert.ok(true, `request is made to GET sys/sync/destinations/${type}/${name} endpoint on find`); - return { - data: { - connection_details: {}, - name, - type, - }, - }; - }); - this.store.findRecord(`sync/destinations/${type}`, name); - } - }); - - test('it calls the correct endpoint for query', async function (assert) { - assert.expect(2); - - this.server.get('sys/sync/destinations', (schema, req) => { - assert.propEqual(req.queryParams, { list: 'true' }, 'it passes { list: true } as query params'); - assert.ok(true, `request is made to LIST sys/sync/destinations endpoint on query`); - return { - data: { - key_info: { - 'aws-sm/': ['my-dest-1'], - 'gh/': ['my-dest-1'], - }, - keys: ['aws-sm/', 'gh/'], - }, - }; - }); - this.store.query('sync/destination', {}); - }); - - test('it should make request to correct endpoint and serialize response for normalizedQuery', async function (assert) { - assert.expect(2); - - this.server.get('sys/sync/destinations', () => { - assert.ok(true, `request is made to LIST sys/sync/destinations endpoint on normalizedQuery`); - return { - data: { - key_info: { - 'aws-sm': ['my-dest-1'], - gh: ['my-dest-1'], - }, - keys: ['aws-sm', 'gh'], - }, - }; - }); - - const spy = sinon.spy(this.store.serializerFor('sync/destination'), 'extractLazyPaginatedData'); - await this.store.adapterFor('sync/destination').normalizedQuery(); - assert.true(spy.calledOnce, 'Serializer method used on response'); - }); - - test('it should make request to correct endpoint for deleteRecord with base model', async function (assert) { - assert.expect(2); - - this.server.delete('/sys/sync/destinations/aws-sm/us-west-1', (schema, req) => { - assert.ok(true, 'DELETE request made to correct endpoint'); - assert.propEqual(req.queryParams, { purge: 'true' }, 'Purge query param is passed in request'); - return {}; - }); - - const modelName = 'sync/destination'; - this.store.pushPayload(modelName, { - modelName, - type: 'aws-sm', - name: 'us-west-1', - id: 'us-west-1', - }); - const model = this.store.peekRecord(modelName, 'us-west-1'); - await model.destroyRecord(); - }); - - test('it should make request to correct endpoint for deleteRecord', async function (assert) { - assert.expect(2); - - const destination = this.server.create('sync-destination', 'aws-sm'); - - this.server.delete(`/sys/sync/destinations/${destination.type}/${destination.name}`, (schema, req) => { - assert.ok(true, 'DELETE request made to correct endpoint'); - assert.propEqual(req.queryParams, { purge: 'true' }, 'Purge query param is passed in request'); - return {}; - }); - - const modelName = 'sync/destinations/aws-sm'; - this.store.pushPayload(modelName, { - modelName, - ...destination, - id: destination.name, - }); - const model = this.store.peekRecord(modelName, destination.name); - await model.destroyRecord(); - }); -}); diff --git a/ui/tests/unit/serializers/sync/associations-test.js b/ui/tests/unit/serializers/sync/associations-test.js deleted file mode 100644 index bf5c7d9153..0000000000 --- a/ui/tests/unit/serializers/sync/associations-test.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupTest } from 'vault/tests/helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; - -module('Unit | Serializer | sync | association', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - this.serializer = this.owner.lookup('serializer:sync/association'); - }); - - test('it normalizes query payload from the server', async function (assert) { - const updated_at = '2023-09-20T10:51:53.961861096-04:00'; - const destinationName = 'us-west-1'; - const destinationType = 'aws-sm'; - const associations = [ - { mount: 'foo', secret_name: 'bar', sync_status: 'SYNCED', updated_at }, - { mount: 'test', secret_name: 'my-secret', sync_status: 'UNSYNCED', updated_at }, - ]; - const payload = { - data: { - associated_secrets: { - 'foo_12345/bar': associations[0], - 'test_12345/my-secret': associations[1], - }, - store_name: destinationName, - store_type: destinationType, - }, - }; - const expected = [ - { id: 'foo/bar', destinationName, destinationType, ...associations[0] }, - { id: 'test/my-secret', destinationName, destinationType, ...associations[1] }, - ]; - const normalized = this.serializer.extractLazyPaginatedData(payload); - - assert.deepEqual(normalized, expected, 'lazy paginated data is extracted from payload'); - }); - - test('it should normalize response for fetchByDestinations request', async function (assert) { - const payload = { - data: { - associated_secrets: { - 'foo_12345/bar': { - mount: 'foo', - secret_name: 'bar', - sync_status: 'SYNCED', - updated_at: '2023-09-20T10:51:53.961861096-04:00', - }, - 'bar_12345/baz': { - mount: 'bar', - secret_name: 'baz', - sync_status: 'UNSYNCED', - updated_at: '2023-11-30T14:51:53.961861096-04:00', - }, - }, - store_name: 'us-west-1', - store_type: 'aws-sm', - }, - }; - const expected = { - icon: 'aws-color', - name: 'us-west-1', - type: 'aws-sm', - associationCount: 2, - status: '1 Unsynced', - lastUpdated: new Date(payload.data.associated_secrets['bar_12345/baz'].updated_at), - }; - let normalized = this.serializer.normalizeFetchByDestinations(payload); - - assert.deepEqual(normalized, expected, 'Response is normalized from fetchByDestinations request'); - - payload.data.associated_secrets['bar_12345/baz'].sync_status = 'SYNCED'; - normalized = this.serializer.normalizeFetchByDestinations(payload); - - assert.strictEqual( - normalized.status, - 'All synced', - 'Correct status is set when all associations are synced' - ); - }); -}); diff --git a/ui/tests/unit/serializers/sync/destinations-test.js b/ui/tests/unit/serializers/sync/destinations-test.js deleted file mode 100644 index 844e15cb07..0000000000 --- a/ui/tests/unit/serializers/sync/destinations-test.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupTest } from 'vault/tests/helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { destinationTypes } from 'vault/helpers/sync-destinations'; - -module('Unit | Serializer | sync | destination', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - this.serializer = this.owner.lookup('serializer:sync/destination'); - }); - - test('it normalizes findRecord payload from the server', async function (assert) { - const types = destinationTypes(); - assert.expect(types.length); - - for (const destinationType of types) { - const { name, type, id, ...connection_details } = this.server.create( - 'sync-destination', - destinationType - ); - const serverData = { request_id: id, data: { name, type, connection_details } }; - - const normalized = this.serializer._normalizePayload(serverData); - const expected = { data: { id: name, type, name, ...connection_details } }; - assert.propEqual( - normalized, - expected, - `generates id and adds connection details to ${destinationType} data object` - ); - } - }); - - test('it normalizes query payload from the server', async function (assert) { - assert.expect(1); - // hardcoded from docs https://developer.hashicorp.com/vault/api-docs/system/secrets-sync#sample-response - // destinations intentionally named the same to test no id naming collision happens - const serverData = { - data: { - key_info: { - 'aws-sm/': ['my-dest-1'], - 'gh/': ['my-dest-1'], - }, - keys: ['aws-sm/', 'gh/'], - }, - }; - - const normalized = this.serializer.extractLazyPaginatedData(serverData); - const expected = [ - { - id: 'aws-sm/my-dest-1', - name: 'my-dest-1', - type: 'aws-sm', - }, - { - id: 'gh/my-dest-1', - name: 'my-dest-1', - type: 'gh', - }, - ]; - - assert.propEqual(normalized, expected, 'payload is array of objects with concatenated type/name as id'); - }); -});