Secrets Sync: Bug fixes part 1 (#24580)

This commit is contained in:
claire bontempo
2023-12-20 13:08:53 -08:00
committed by GitHub
parent 1384aefc69
commit 0529b11571
10 changed files with 65 additions and 16 deletions

View File

@@ -29,7 +29,8 @@ export default class SyncAssociationAdapter extends ApplicationAdapter {
// typically associations are queried for a specific destination which is what the standard query method does // 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 // in specific cases we can query all associations to access total_associations and total_secrets values
queryAll() { queryAll() {
return this.query(this.store, { modelName: 'sync/association' }).then((response) => { const url = `${this.buildURL('sync/association')}`;
return this.ajax(url, 'GET', { data: { list: true } }).then((response) => {
const { total_associations, total_secrets } = response.data; const { total_associations, total_secrets } = response.data;
return { total_associations, total_secrets }; return { total_associations, total_secrets };
}); });
@@ -49,8 +50,8 @@ export default class SyncAssociationAdapter extends ApplicationAdapter {
// array of association data for each destination a secret is synced to // array of association data for each destination a secret is synced to
fetchSyncStatus({ mount, secretName }) { fetchSyncStatus({ mount, secretName }) {
const url = `${this.buildURL()}/${mount}/${secretName}`; const url = `${this.buildURL()}/destinations`;
return this.ajax(url, 'GET').then((resp) => { return this.ajax(url, 'GET', { data: { mount, secret_name: secretName } }).then((resp) => {
const { associated_destinations } = resp.data; const { associated_destinations } = resp.data;
const syncData = []; const syncData = [];
for (const key in associated_destinations) { for (const key in associated_destinations) {

View File

@@ -36,10 +36,11 @@
<LoadingDropdownOption /> <LoadingDropdownOption />
</li> </li>
{{else}} {{else}}
<li class="is-inline-block"> <li class="action">
<Hds::Button <Hds::Button
@text="Sync now" @text="Sync now"
class="link" class="link is-flex-start"
@isFullWidth={{true}}
disabled={{not association.canSync}} disabled={{not association.canSync}}
data-test-association-action="sync" data-test-association-action="sync"
{{on "click" (fn this.update association "set")}} {{on "click" (fn this.update association "set")}}
@@ -60,7 +61,7 @@
data-test-association-action="unsync" data-test-association-action="unsync"
@isInDropdown={{true}} @isInDropdown={{true}}
@buttonText="Unsync" @buttonText="Unsync"
@confirmMessage="This secret will be unsynced from all destinations." @confirmMessage="This secret will be unsynced from this destination."
@onConfirmAction={{fn this.update association "remove"}} @onConfirmAction={{fn this.update association "remove"}}
/> />
{{/if}} {{/if}}

View File

@@ -39,9 +39,14 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
async update(association: SyncAssociationModel, operation: string) { async update(association: SyncAssociationModel, operation: string) {
try { try {
await association.save({ adapterOptions: { action: operation } }); await association.save({ adapterOptions: { action: operation } });
// this message can be expanded after testing -- deliberately generic for now const action: string = operation === 'set' ? 'Sync' : 'Unsync';
this.flashMessages.success( this.flashMessages.success(`${action} operation initiated.`);
'Sync operation successfully initiated. Status will be updated on secret when complete.' // refresh route to update displayed secrets
this.store.clearDataset('sync/association');
this.router.transitionTo(
'vault.cluster.sync.secrets.destinations.destination.secrets',
this.args.destination.type,
this.args.destination.name
); );
} catch (error) { } catch (error) {
this.flashMessages.danger(`Sync operation error: \n ${errorMessage(error)}`); this.flashMessages.danger(`Sync operation error: \n ${errorMessage(error)}`);

View File

@@ -74,10 +74,15 @@ export default class DestinationSyncPageComponent extends Component<Args> {
@action @action
setMount(selected: Array<string>) { setMount(selected: Array<string>) {
this.mountPath = selected[0] || ''; this.mountPath = selected[0] || '';
if (this.mountPath === '') {
// clear secret path when mount is cleared
this.secretPath = '';
}
} }
setAssociation = task({}, async (event: Event) => { setAssociation = task({}, async (event: Event) => {
event.preventDefault(); event.preventDefault();
this.error = ''; // reset error
try { try {
this.syncedSecret = ''; this.syncedSecret = '';
const { name: destinationName, type: destinationType } = this.args.destination; const { name: destinationName, type: destinationType } = this.args.destination;

View File

@@ -17,6 +17,12 @@ interface SyncDestinationSecretsRouteParams {
export default class SyncDestinationSecretsRoute extends Route { export default class SyncDestinationSecretsRoute extends Route {
@service declare readonly store: StoreService; @service declare readonly store: StoreService;
queryParams = {
page: {
refreshModel: true,
},
};
model(params: SyncDestinationSecretsRouteParams) { model(params: SyncDestinationSecretsRouteParams) {
const destination = this.modelFor('secrets.destinations.destination') as SyncDestinationModel; const destination = this.modelFor('secrets.destinations.destination') as SyncDestinationModel;
return hash({ return hash({

View File

@@ -19,6 +19,18 @@ interface SyncSecretsDestinationsIndexRouteParams {
export default class SyncSecretsDestinationsIndexRoute extends Route { export default class SyncSecretsDestinationsIndexRoute extends Route {
@service declare readonly store: StoreService; @service declare readonly store: StoreService;
queryParams = {
page: {
refreshModel: true,
},
name: {
refreshModel: true,
},
type: {
refreshModel: true,
},
};
filterData(dataset: Array<SyncDestinationModel>, name: string, type: string): Array<SyncDestinationModel> { filterData(dataset: Array<SyncDestinationModel>, name: string, type: string): Array<SyncDestinationModel> {
let filteredDataset = dataset; let filteredDataset = dataset;
const filter = (key: keyof SyncDestinationModel, value: string) => { const filter = (key: keyof SyncDestinationModel, value: string) => {

View File

@@ -29,7 +29,7 @@ export const associationsResponse = (schema, req) => {
}; };
export const syncStatusResponse = (schema, req) => { export const syncStatusResponse = (schema, req) => {
const { mount, name: secret_name } = req.params; const { mount, secret_name } = req.queryParams;
const records = schema.db.syncAssociations.where({ mount, secret_name }); const records = schema.db.syncAssociations.where({ mount, secret_name });
if (!records.length) { if (!records.length) {
return new Response(404, {}, { errors: [] }); return new Response(404, {}, { errors: [] });

View File

@@ -99,9 +99,17 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
}); });
test('it renders secret details and toggles json view', async function (assert) { test('it renders secret details and toggles json view', async function (assert) {
assert.expect(8); assert.expect(9);
this.server.get(`sys/sync/associations/:mount/*name`, (schema, req) => { this.server.get(`sys/sync/associations/destinations`, (schema, req) => {
assert.ok(true, 'request made to fetch sync status'); assert.ok(true, 'request made to fetch sync status');
assert.propEqual(
req.queryParams,
{
mount: this.backend,
secret_name: this.path,
},
'query params include mount and secret name'
);
// no records so response returns 404 // no records so response returns 404
return syncStatusResponse(schema, req); return syncStatusResponse(schema, req);
}); });
@@ -233,7 +241,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
}); });
test('it renders sync status page alert', async function (assert) { test('it renders sync status page alert', async function (assert) {
assert.expect(3); // assert count important because confirms request made to fetch sync status twice assert.expect(5); // assert count important because confirms request made to fetch sync status twice
const destinationName = 'my-destination'; const destinationName = 'my-destination';
this.server.create('sync-association', { this.server.create('sync-association', {
type: 'aws-sm', type: 'aws-sm',
@@ -241,9 +249,17 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
mount: this.backend, mount: this.backend,
secret_name: this.path, secret_name: this.path,
}); });
this.server.get('sys/sync/associations/:mount/*name', (schema, req) => { this.server.get(`sys/sync/associations/destinations`, (schema, req) => {
// this assertion should be hit twice, once on init and again when the 'Refresh' button is clicked // this assertion should be hit twice, once on init and again when the 'Refresh' button is clicked
assert.ok(true, 'request made to fetch sync status'); assert.ok(true, 'request made to fetch sync status');
assert.propEqual(
req.queryParams,
{
mount: this.backend,
secret_name: this.path,
},
'query params include mount and secret name'
);
return syncStatusResponse(schema, req); return syncStatusResponse(schema, req);
}); });

View File

@@ -56,6 +56,8 @@ module('Integration | Component | sync | Secrets::Page::Destinations::Destinatio
await click(kvSuggestion.input); await click(kvSuggestion.input);
assert.dom(searchSelect.option()).hasText('my-path/', 'Nested secret path renders'); assert.dom(searchSelect.option()).hasText('my-path/', 'Nested secret path renders');
assert.dom(searchSelect.option(1)).hasText('my-secret', 'Secret renders'); assert.dom(searchSelect.option(1)).hasText('my-secret', 'Secret renders');
await click(searchSelect.removeSelected);
assert.dom(kvSuggestion.input).hasValue('', 'secret path value is cleared when mount is unset');
}); });
test('it should render secret suggestions for nested paths', async function (assert) { test('it should render secret suggestions for nested paths', async function (assert) {

View File

@@ -59,10 +59,11 @@ module('Unit | Adapter | sync | association', function (hooks) {
}); });
test('it should make request to correct endpoint for queryAll associations', async function (assert) { test('it should make request to correct endpoint for queryAll associations', async function (assert) {
assert.expect(2); assert.expect(3);
this.server.get('/sys/sync/associations', () => { this.server.get('/sys/sync/associations', (schema, req) => {
assert.ok(true, 'request is made to correct endpoint for queryAll'); assert.ok(true, 'request is made to correct endpoint for queryAll');
assert.propEqual(req.queryParams, { list: 'true' }, 'query params include list: true');
return { return {
data: { data: {
key_info: {}, key_info: {},