diff --git a/addons/api/addon/adapters/application.js b/addons/api/addon/adapters/application.js index 65c8c2c6d..d69a5f043 100644 --- a/addons/api/addon/adapters/application.js +++ b/addons/api/addon/adapters/application.js @@ -283,7 +283,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend( const baseError = { isInvalid: true, status, - code: payload.code, + code: payload.kind, detail: payload.message, source: { pointer: '/data' }, }; diff --git a/addons/api/addon/handlers/indexed-db-handler.js b/addons/api/addon/handlers/indexed-db-handler.js index 2946ef893..99e582e33 100644 --- a/addons/api/addon/handlers/indexed-db-handler.js +++ b/addons/api/addon/handlers/indexed-db-handler.js @@ -3,6 +3,7 @@ import { inject as service } from '@ember/service'; import { getOwner, setOwner } from '@ember/application'; import { queryIndexedDb } from '../utils/indexed-db-query'; import { paginateResults } from '../utils/paginate-results'; +import { hashCode } from '../utils/hash-code'; /** * Handler to sit in front of the API request layer @@ -34,9 +35,10 @@ export default class IndexedDbHandler { } let { page, pageSize, query: queryObj, ...remainingQuery } = query; + const tokenKey = `${type}-${hashCode(remainingQuery)}`; if (!peekIndexedDB) { - const listToken = await indexedDb.token.get(type); + const listToken = await indexedDb.token.get(tokenKey); const adapter = store.adapterFor(type); const schema = store.modelFor(type); @@ -44,35 +46,41 @@ export default class IndexedDbHandler { try { payload = await adapter.query(store, schema, { ...remainingQuery, - scope_id: 'global', - recursive: true, list_token: listToken?.token, + }); + } catch (e) { + // If we get an invalid list token, we'll delete the token and + // clear the DB and try again without a token. + if ( + e?.errors[0].status === 400 && + e?.errors[0].code === 'invalid list token' + ) { + await indexedDb[type].clear(); + // Delete all tokens of the same resource type so we don't keep clearing the DB. + // Even if other tokens may still be valid, we might have cleared data that they + // depended on so we should clear all tokens of the same type when clearing the DB. + const tokenKeys = await indexedDb.token + .where(':id') + .startsWith(type) + .primaryKeys(); + + await indexedDb.token.bulkDelete(tokenKeys); + + payload = await adapter.query(store, schema, { + ...remainingQuery, }); - } catch (e) { - // If we get any error that's not a rate limiting error, - // assume the token is not good anymore. We'll delete the token and - // clear the DB and try again without a token. - // TODO: Probably should check for 400 and bad token error specifically instead? - if (e?.errors[0].status !== 429) { - await indexedDb[type].clear(); - await indexedDb.token.delete(type); - - payload = await adapter.query(store, schema, { - ...remainingQuery, - scope_id: 'global', - recursive: true, - list_token: null, - }); - } else { - throw e; - } + } else { + throw e; } + } - // Store the token we just got back from the payload + // Store the token we just got back from the payload if it exists + if (payload.list_token) { await indexedDb.token.put({ - id: type, + id: tokenKey, token: payload.list_token, }); + } // Remove any records from the DB if the API indicates they've been deleted if (payload.removed_ids?.length > 0) { diff --git a/addons/api/addon/utils/hash-code.js b/addons/api/addon/utils/hash-code.js new file mode 100644 index 000000000..95df3aba3 --- /dev/null +++ b/addons/api/addon/utils/hash-code.js @@ -0,0 +1,24 @@ +/** + * Takes an object and gets the JSON stringified version of it to be converted + * to a hash code. Taken from https://stackoverflow.com/a/8076436 + * @param obj + */ +export const hashCode = (obj) => { + if (!obj) { + return 0; + } + + // Use a replacer function to sort the keys of the object so we get the same hashcode + // regardless of the order of the keys. + const jsonString = JSON.stringify(obj, Object.keys(obj).sort()); + + let hash = 0; + for (let i = 0; i < jsonString.length; i++) { + let code = jsonString.charCodeAt(i); + hash = (hash << 5) - hash + code; + hash = hash & hash; // Convert to 32bit integer + } + + // Make sure the hash is positive since we're using it as an ID + return Math.abs(hash); +}; diff --git a/addons/api/tests/unit/utils/hash-code-test.js b/addons/api/tests/unit/utils/hash-code-test.js new file mode 100644 index 000000000..ac80fa4bf --- /dev/null +++ b/addons/api/tests/unit/utils/hash-code-test.js @@ -0,0 +1,36 @@ +import { hashCode } from 'api/utils/hash-code'; +import { module, test } from 'qunit'; + +module('Unit | Utility | hash-code', function () { + test('it hashes', function (assert) { + const result = hashCode({ a: 1, b: 2 }); + assert.ok(result); + }); + + test('it hashes to same result regardless of object order', function (assert) { + const first = hashCode({ a: 1, b: 2, c: 3 }); + const second = hashCode({ b: 2, c: 3, a: 1 }); + assert.strictEqual(first, second); + }); + + test('it hashes to different result if objects are different', function (assert) { + const first = hashCode({ a: 1, b: 2, c: 3 }); + const second = hashCode({ a: '1', b: '2', c: '3' }); + assert.notStrictEqual(first, second); + }); + + test('it hashes an empty object', function (assert) { + const result = hashCode({}); + assert.ok(result); + }); + + test('it hashes null to 0', function (assert) { + const result = hashCode(null); + assert.strictEqual(result, 0); + }); + + test('it hashes undefined to 0', function (assert) { + const result = hashCode(undefined); + assert.strictEqual(result, 0); + }); +}); diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/index.js b/ui/admin/app/routes/scopes/scope/credential-stores/index.js index c7ac59114..ae393a6e0 100644 --- a/ui/admin/app/routes/scopes/scope/credential-stores/index.js +++ b/ui/admin/app/routes/scopes/scope/credential-stores/index.js @@ -58,6 +58,7 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route { }) ) { credentialStores = await this.store.query('credential-store', { + scope_id, query: { search, filters }, page, pageSize, @@ -86,6 +87,7 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route { const credentialStore = await this.store.query( 'credential-store', { + scope_id, query: { filters: { scope_id: [{ equals: scope_id }] } }, page: 1, pageSize: 1, diff --git a/ui/admin/app/routes/scopes/scope/groups/index.js b/ui/admin/app/routes/scopes/scope/groups/index.js index fa6f930f1..8681b6c5f 100644 --- a/ui/admin/app/routes/scopes/scope/groups/index.js +++ b/ui/admin/app/routes/scopes/scope/groups/index.js @@ -43,6 +43,7 @@ export default class ScopesScopeGroupsIndexRoute extends Route { if (this.can.can('list model', scope, { collection: 'groups' })) { groups = await this.store.query('group', { + scope_id, query: { filters, search }, page, pageSize, @@ -67,6 +68,7 @@ export default class ScopesScopeGroupsIndexRoute extends Route { const group = await this.store.query( 'group', { + scope_id: scope_id, query: { filters: { scope_id: [{ equals: scope_id }], diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/index.js b/ui/admin/app/routes/scopes/scope/host-catalogs/index.js index 12c8334fd..7566471a5 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/index.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/index.js @@ -48,6 +48,7 @@ export default class ScopesScopeHostCatalogsIndexRoute extends Route { let hostCatalogsExist = false; if (this.can.can('list model', scope, { collection: 'host-catalogs' })) { hostCatalogs = await this.store.query('host-catalog', { + scope_id, query: { search, filters }, page, pageSize, @@ -74,6 +75,7 @@ export default class ScopesScopeHostCatalogsIndexRoute extends Route { const hostCatalogs = await this.store.query( 'host-catalog', { + scope_id, query: { filters: { scope_id: [{ equals: scope_id }] } }, page: 1, pageSize: 1, diff --git a/ui/admin/app/routes/scopes/scope/roles/index.js b/ui/admin/app/routes/scopes/scope/roles/index.js index 7ce202aed..fc7fcda79 100644 --- a/ui/admin/app/routes/scopes/scope/roles/index.js +++ b/ui/admin/app/routes/scopes/scope/roles/index.js @@ -45,6 +45,7 @@ export default class ScopesScopeRolesIndexRoute extends Route { let rolesExist = false; if (this.can.can('list model', scope, { collection: 'roles' })) { roles = await this.store.query('role', { + scope_id, query: { search, filters }, page, pageSize, @@ -70,6 +71,7 @@ export default class ScopesScopeRolesIndexRoute extends Route { const role = await this.store.query( 'role', { + scope_id, query: { filters: { scope_id: [{ equals: scope_id }], diff --git a/ui/admin/app/routes/scopes/scope/sessions/index.js b/ui/admin/app/routes/scopes/scope/sessions/index.js index 41e239d5b..4ae3e6876 100644 --- a/ui/admin/app/routes/scopes/scope/sessions/index.js +++ b/ui/admin/app/routes/scopes/scope/sessions/index.js @@ -67,6 +67,7 @@ export default class ScopesScopeSessionsIndexRoute extends Route { }); const queryOptions = { + scope_id, include_terminated: true, query: { search, filters }, page, @@ -84,7 +85,7 @@ export default class ScopesScopeSessionsIndexRoute extends Route { await this.getAssociatedUsers(); } if (!this.associatedTargets) { - await this.getAssociatedTargets(); + await this.getAssociatedTargets(scope_id); } return { @@ -103,6 +104,7 @@ export default class ScopesScopeSessionsIndexRoute extends Route { */ async getAllSessions(scope_id) { const allSessionsQuery = { + scope_id, include_terminated: true, query: { filters: { scope_id: [{ equals: scope_id }] } }, }; @@ -128,6 +130,8 @@ export default class ScopesScopeSessionsIndexRoute extends Route { filters.id.values.push({ equals: userId }); }); const associatedUsersQuery = { + scope_id: 'global', + recursive: true, query: { filters }, }; this.associatedUsers = await this.store.query('user', associatedUsersQuery); @@ -135,9 +139,10 @@ export default class ScopesScopeSessionsIndexRoute extends Route { /** * Get all the targets but only load them once when entering the route. + * @param scope_id * @returns {Promise} */ - async getAssociatedTargets() { + async getAssociatedTargets(scope_id) { const uniqueSessionTargetIds = new Set( this.allSessions .filter((session) => session.target_id) @@ -148,6 +153,7 @@ export default class ScopesScopeSessionsIndexRoute extends Route { filters.id.values.push({ equals: targetId }); }); const associatedTargetsQuery = { + scope_id, query: { filters }, }; this.associatedTargets = await this.store.query( diff --git a/ui/admin/app/routes/scopes/scope/targets/index.js b/ui/admin/app/routes/scopes/scope/targets/index.js index a9d42527a..556f12576 100644 --- a/ui/admin/app/routes/scopes/scope/targets/index.js +++ b/ui/admin/app/routes/scopes/scope/targets/index.js @@ -61,6 +61,7 @@ export default class ScopesScopeTargetsIndexRoute extends Route { }); const sessions = await this.store.query('session', { + scope_id, query: { filters: { scope_id: [{ equals: scope_id }], @@ -78,6 +79,7 @@ export default class ScopesScopeTargetsIndexRoute extends Route { let targetsExist = false; if (this.can.can('list model', scope, { collection: 'targets' })) { targets = await this.store.query('target', { + scope_id, query: { search, filters }, page, pageSize, diff --git a/ui/admin/app/routes/scopes/scope/users/index.js b/ui/admin/app/routes/scopes/scope/users/index.js index 24d5e1422..ff2495328 100644 --- a/ui/admin/app/routes/scopes/scope/users/index.js +++ b/ui/admin/app/routes/scopes/scope/users/index.js @@ -46,6 +46,7 @@ export default class ScopesScopeUsersIndexRoute extends Route { let usersExist = false; if (this.can.can('list model', scope, { collection: 'users' })) { users = await this.store.query('user', { + scope_id, query: { search, filters }, page, pageSize, @@ -59,11 +60,11 @@ export default class ScopesScopeUsersIndexRoute extends Route { /** * Sets usersExist to true if there exists any users. - * @param {string} scopeId + * @param {string} scope_id * @param {number} totalItems * @returns */ - async getUsersExist(scopeId, totalItems) { + async getUsersExist(scope_id, totalItems) { if (totalItems > 0) { return true; } @@ -71,9 +72,10 @@ export default class ScopesScopeUsersIndexRoute extends Route { const user = await this.store.query( 'user', { + scope_id, query: { filters: { - scope_id: [{ equals: scopeId }], + scope_id: [{ equals: scope_id }], }, }, page: 1, diff --git a/ui/admin/app/templates/scopes/scope/targets/index.hbs b/ui/admin/app/templates/scopes/scope/targets/index.hbs index f2a9ba90e..368e7d33e 100644 --- a/ui/admin/app/templates/scopes/scope/targets/index.hbs +++ b/ui/admin/app/templates/scopes/scope/targets/index.hbs @@ -4,7 +4,6 @@ }} - @@ -22,9 +21,10 @@ {{#if (can 'create model' this.scope collection='targets')}} {{#if @model.targetsExist}} - {{t - 'resources.target.titles.new' - }} + {{/if}} {{/if}} diff --git a/ui/admin/tests/acceptance/targets/create-test.js b/ui/admin/tests/acceptance/targets/create-test.js index b71540213..8791832b5 100644 --- a/ui/admin/tests/acceptance/targets/create-test.js +++ b/ui/admin/tests/acceptance/targets/create-test.js @@ -22,10 +22,12 @@ import { //invalidateSession, } from 'ember-simple-auth/test-support'; import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; module('Acceptance | targets | create', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let getTargetCount; let getTCPTargetCount;