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
// in specific cases we can query all associations to access total_associations and total_secrets values
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;
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
fetchSyncStatus({ mount, secretName }) {
const url = `${this.buildURL()}/${mount}/${secretName}`;
return this.ajax(url, 'GET').then((resp) => {
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) {

View File

@@ -36,10 +36,11 @@
<LoadingDropdownOption />
</li>
{{else}}
<li class="is-inline-block">
<li class="action">
<Hds::Button
@text="Sync now"
class="link"
class="link is-flex-start"
@isFullWidth={{true}}
disabled={{not association.canSync}}
data-test-association-action="sync"
{{on "click" (fn this.update association "set")}}
@@ -60,7 +61,7 @@
data-test-association-action="unsync"
@isInDropdown={{true}}
@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"}}
/>
{{/if}}

View File

@@ -39,9 +39,14 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
async update(association: SyncAssociationModel, operation: string) {
try {
await association.save({ adapterOptions: { action: operation } });
// this message can be expanded after testing -- deliberately generic for now
this.flashMessages.success(
'Sync operation successfully initiated. Status will be updated on secret when complete.'
const action: string = operation === 'set' ? 'Sync' : 'Unsync';
this.flashMessages.success(`${action} operation initiated.`);
// 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) {
this.flashMessages.danger(`Sync operation error: \n ${errorMessage(error)}`);

View File

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

View File

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

View File

@@ -19,6 +19,18 @@ interface SyncSecretsDestinationsIndexRouteParams {
export default class SyncSecretsDestinationsIndexRoute extends Route {
@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> {
let filteredDataset = dataset;
const filter = (key: keyof SyncDestinationModel, value: string) => {

View File

@@ -29,7 +29,7 @@ export const associationsResponse = (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 });
if (!records.length) {
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) {
assert.expect(8);
this.server.get(`sys/sync/associations/:mount/*name`, (schema, req) => {
assert.expect(9);
this.server.get(`sys/sync/associations/destinations`, (schema, req) => {
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
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) {
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';
this.server.create('sync-association', {
type: 'aws-sm',
@@ -241,9 +249,17 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
mount: this.backend,
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
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);
});

View File

@@ -56,6 +56,8 @@ module('Integration | Component | sync | Secrets::Page::Destinations::Destinatio
await click(kvSuggestion.input);
assert.dom(searchSelect.option()).hasText('my-path/', 'Nested secret path 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) {

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) {
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.propEqual(req.queryParams, { list: 'true' }, 'query params include list: true');
return {
data: {
key_info: {},