From 4c7ea161cd6981530cd320f1b19f3b559a47e299 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:18:04 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20sqlite=20support?= =?UTF-8?q?=20to=20sessions=20(#2978)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 Add sqlite support to sessions * refactor: 💡 only peek sqlite db when batching queries ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17411 --- addons/api/addon/services/sqlite.js | 11 ++ addons/api/addon/workers/utils/schema.js | 50 +++++-- .../app/routes/scopes/scope/sessions/index.js | 126 +++++++++++++----- 3 files changed, 144 insertions(+), 43 deletions(-) diff --git a/addons/api/addon/services/sqlite.js b/addons/api/addon/services/sqlite.js index 1567d5664..c586dd87e 100644 --- a/addons/api/addon/services/sqlite.js +++ b/addons/api/addon/services/sqlite.js @@ -99,6 +99,16 @@ export const modelMapping = { target_scope_name: 'create_time_values.target.scope.name', created_time: 'created_time', }, + session: { + id: 'id', + type: 'type', + status: 'status', + endpoint: 'endpoint', + target_id: 'target_id', + user_id: 'user_id', + scope_id: 'scope.scope_id', + created_time: 'created_time', + }, }; // A list of tables that we support searching using FTS5 in SQLite. @@ -113,6 +123,7 @@ export const searchTables = new Set([ 'auth-method', 'host-catalog', 'session-recording', + 'session', ]); export default class SqliteDbService extends Service { diff --git a/addons/api/addon/workers/utils/schema.js b/addons/api/addon/workers/utils/schema.js index 501d887cb..7b1f29994 100644 --- a/addons/api/addon/workers/utils/schema.js +++ b/addons/api/addon/workers/utils/schema.js @@ -388,6 +388,45 @@ CREATE TRIGGER IF NOT EXISTS session_recording_ad AFTER DELETE ON session_record VALUES('delete', old.rowid, old.id, old.type, old.state, old.start_time, old.end_time, old.duration, old.scope_id, old.user_id, old.user_name, old.target_id, old.target_name, old.target_scope_id, old.target_scope_name, old.created_time); END;`; +const createSessionTables = ` +CREATE TABLE IF NOT EXISTS session ( + id TEXT NOT NULL PRIMARY KEY, + type TEXT, + status TEXT, + endpoint TEXT, + target_id TEXT, + user_id TEXT, + scope_id TEXT NOT NULL, + created_time TEXT NOT NULL, + data TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_session_scope_id_created_time ON session(scope_id, created_time DESC); + +CREATE VIRTUAL TABLE IF NOT EXISTS session_fts USING fts5( + id, + type, + status, + endpoint, + target_id, + user_id, + scope_id, + created_time, + content='', +); + +CREATE TRIGGER IF NOT EXISTS session_ai AFTER INSERT ON session BEGIN + INSERT INTO session_fts( + id, type, status, endpoint, target_id, user_id, scope_id, created_time + ) VALUES ( + new.id, new.type, new.status, new.endpoint, new.target_id, new.user_id, new.scope_id, new.created_time + ); +END; + +CREATE TRIGGER IF NOT EXISTS session_ad AFTER DELETE ON session BEGIN + INSERT INTO session_fts(session_fts, rowid, id, type, status, endpoint, target_id, user_id, scope_id, created_time) + VALUES('delete', old.rowid, old.id, old.type, old.status, old.endpoint, old.target_id, old.user_id, old.scope_id, old.created_time); +END;`; + export const CREATE_TABLES = (version) => ` BEGIN; @@ -408,16 +447,7 @@ ${createScopeTables} ${createAuthMethodTables} ${createHostCatalogTables} ${createSessionRecordingTables} - -CREATE TABLE IF NOT EXISTS "group" ( - id TEXT NOT NULL PRIMARY KEY, - name TEXT, - description TEXT, - scope_id TEXT NOT NULL, - created_time TEXT NOT NULL, - data TEXT NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_group_scope_id_created_time ON "group"(scope_id, created_time DESC); +${createSessionTables} COMMIT;`; diff --git a/ui/admin/app/routes/scopes/scope/sessions/index.js b/ui/admin/app/routes/scopes/scope/sessions/index.js index 99cdb3489..0db68e505 100644 --- a/ui/admin/app/routes/scopes/scope/sessions/index.js +++ b/ui/admin/app/routes/scopes/scope/sessions/index.js @@ -7,6 +7,13 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { action } from '@ember/object'; import { restartableTask, timeout } from 'ember-concurrency'; +import chunk from 'lodash/chunk'; + +// Maximum expression tree depth is 1000 so +// we limit the number of expressions in a query. +// See "Maximum Depth Of An Expression Tree" in +// https://www.sqlite.org/limits.html +const MAX_EXPR_NUM = 999; export default class ScopesScopeSessionsIndexRoute extends Route { // =services @@ -114,10 +121,11 @@ export default class ScopesScopeSessionsIndexRoute extends Route { const totalItems = sessions.meta?.totalItems; const allSessions = await this.getAllSessions(scope_id); - const [associatedUsers, associatedTargets] = await Promise.all([ - this.getAssociatedUsers(scope, allSessions), - this.getAssociatedTargets(scope, allSessions), - ]); + const associatedUsers = await this.getAssociatedUsers(scope, allSessions); + const associatedTargets = await this.getAssociatedTargets( + scope, + allSessions, + ); return { sessions, @@ -159,24 +167,45 @@ export default class ScopesScopeSessionsIndexRoute extends Route { this.can.can('list model', globalScope, { collection: 'users' }) && this.can.can('list model', orgScope, { collection: 'users' }) ) { - const uniqueSessionUserIds = new Set( - allSessions - .filter((session) => session.user_id) - .map((session) => session.user_id), + const users = await this.store.query( + 'user', + { + scope_id: 'global', + recursive: true, + page: 1, + pageSize: 1, + }, + { pushToStore: false }, ); - const filters = { - id: { values: [] }, - }; - uniqueSessionUserIds.forEach((userId) => { - filters.id.values.push({ equals: userId }); - }); - const associatedUsersQuery = { - scope_id: 'global', - recursive: true, - query: { filters }, - }; - return this.store.query('user', associatedUsersQuery); + if (!users.length) { + return []; + } + + const uniqueSessionUserIds = [ + ...new Set( + allSessions + .filter((session) => session.user_id) + .map((session) => session.user_id), + ), + ]; + const chunkedUserIds = chunk(uniqueSessionUserIds, MAX_EXPR_NUM); + const associatedUsersPromises = chunkedUserIds.map((userIds) => + this.store.query( + 'user', + { + scope_id: 'global', + query: { + filters: { + id: { values: userIds.map((userId) => ({ equals: userId })) }, + }, + }, + }, + { peekDb: true }, + ), + ); + const usersArray = await Promise.all(associatedUsersPromises); + return usersArray.flat(); } return []; @@ -189,21 +218,52 @@ export default class ScopesScopeSessionsIndexRoute extends Route { */ async getAssociatedTargets(scope, allSessions) { if (this.can.can('list model', scope, { collection: 'targets' })) { - const uniqueSessionTargetIds = new Set( - allSessions - .filter((session) => session.target_id) - .map((session) => session.target_id), + const targets = await this.store.query( + 'target', + { + scope_id: scope.id, + query: { + filters: { + scope_id: [{ equals: scope.id }], + }, + }, + page: 1, + pageSize: 1, + }, + { pushToStore: false }, ); - const filters = { id: { values: [] } }; - uniqueSessionTargetIds.forEach((targetId) => { - filters.id.values.push({ equals: targetId }); - }); - const associatedTargetsQuery = { - scope_id: scope.id, - query: { filters }, - }; - return this.store.query('target', associatedTargetsQuery); + if (!targets.length) { + return []; + } + + const uniqueSessionTargetIds = [ + ...new Set( + allSessions + .filter((session) => session.target_id) + .map((session) => session.target_id), + ), + ]; + + const chunkedTargetIds = chunk(uniqueSessionTargetIds, MAX_EXPR_NUM); + const associatedTargetsPromises = chunkedTargetIds.map((targetIds) => + this.store.query( + 'target', + { + scope_id: scope.id, + query: { + filters: { + id: { + values: targetIds.map((targetId) => ({ equals: targetId })), + }, + }, + }, + }, + { peekDb: true }, + ), + ); + const targetsArray = await Promise.all(associatedTargetsPromises); + return targetsArray.flat(); } return [];