mirror of
https://github.com/hashicorp/vault.git
synced 2026-01-06 09:11:10 +01:00
Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
This commit is contained in:
parent
c5b3edc0e4
commit
06068fb8eb
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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(',');
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user