From da785da2dd2bcfce0e273f0b32e20b0e5492625f Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 3 Nov 2026 06:20:06 +0100 Subject: [PATCH] u --- src/api/routes/company.js | 23 +++++++-- src/api/routes/vfs.js | 35 +++++++++++++ src/db/account.js | 3 ++ src/db/agentprinter.js | 4 ++ src/db/carrier.js | 5 ++ src/db/company.js | 11 +++- src/db/companysetting.js | 14 ++++++ src/db/companyuser.js | 51 ++++++++++++++++++- src/db/customergroup.js | 5 ++ src/db/numbers.js | 2 + src/db/posterminal.js | 3 ++ src/db/printer.js | 5 ++ src/db/printeragent.js | 4 ++ src/db/warehouse.js | 5 ++ src/services/javascriptSandbox.js | 4 +- src/tools/javascript-worker.js | 2 +- src/tools/javascript.js | 13 ++++- src/tools/patch.js | 8 ++- src/tools/reset-vfs.js | 14 +++++- src/tools/rm.js | 7 ++- src/tools/vfs.js | 84 +++++++++++++++++++++++++------ 21 files changed, 273 insertions(+), 29 deletions(-) diff --git a/src/api/routes/company.js b/src/api/routes/company.js index 8241ffb..aef5a05 100644 --- a/src/api/routes/company.js +++ b/src/api/routes/company.js @@ -4,6 +4,7 @@ import multer from 'multer'; import JSZip from 'jszip'; import env from '../../config/env.js'; import * as db from '../../db/company.js'; +import { requireCompanyAdmin } from '../../db/companyuser.js'; import * as googleAuth from '../../db/googleAuth.js'; import { clearCachedUser } from '../middleware/auth.js'; import { emit } from '../sse.js'; @@ -499,13 +500,25 @@ export function register(app, basePath = '') { app.post( `${basePath}/company/import`, + async (req, res, next) => { + try { + const companyId = requireSelectedCompany(req); + await requireCompanyAdmin(req.user.id, companyId); + next(); + } catch (error) { + const status = companyErrorStatus(error); + return res.status(status).json({ error: error.message }); + } + }, uploadCompanyImportFile, async (req, res) => { let companyId; try { companyId = requireSelectedCompany(req); + await requireCompanyAdmin(req.user.id, companyId); } catch (error) { - return res.status(error.statusCode || 400).json({ error: error.message }); + const status = companyErrorStatus(error); + return res.status(status).json({ error: error.message }); } if (activeImportsByCompany.has(companyId)) { @@ -968,7 +981,7 @@ export function register(app, basePath = '') { } try { - const company = await db.updateCompany(parsedId.companyId, body); + const company = await db.updateCompany(parsedId.companyId, body, req.user.id); emit('company', { companyId: parsedId.companyId, data: { companyId: parsedId.companyId } }); res.status(200).json({ company }); } catch (error) { @@ -989,10 +1002,14 @@ export function register(app, basePath = '') { } try { - await db.deleteCompany(parsedId.companyId); + await db.deleteCompany(parsedId.companyId, req.user.id); clearCompanyPrinters(parsedId.companyId); res.sendStatus(204); } catch (error) { + const status = companyErrorStatus(error); + if (status !== 500) { + return res.status(status).json({ error: error.message }); + } console.error('Delete company failed:', error); res.status(500).json({ error: 'Failed to delete company' }); } diff --git a/src/api/routes/vfs.js b/src/api/routes/vfs.js index 68ae1a7..c24b8a5 100644 --- a/src/api/routes/vfs.js +++ b/src/api/routes/vfs.js @@ -2,10 +2,13 @@ import fs from 'fs'; import path from 'path'; import multer from 'multer'; import { emit } from '../sse.js'; +import { requireCompanyAdmin } from '../../db/companyuser.js'; import { prepareVfs, resolveVfsRoot, isReadOnlyDatabasePath, + isProtectedAdminVfsPath, + directoryContainsProtectedAdminVfsFile, getMimeType, restoreDefaultSkills, restoreDefaultApplets, @@ -586,6 +589,8 @@ export function register(app, basePath = '') { : ''; const uploaded = []; + const parsedEntries = []; + let touchesProtected = false; for (const f of files) { const origName = (f.originalname || 'upload').replace(/[\0\\]/g, '/'); const relPath = targetDir ? `${targetDir}/${origName}` : origName; @@ -596,6 +601,18 @@ export function register(app, basePath = '') { const absPath = path.resolve(vfsRoot, parsed.name); if (!absPath.startsWith(vfsRoot)) continue; + + if (isProtectedAdminVfsPath(parsed.name)) { + touchesProtected = true; + } + parsedEntries.push({ f, parsed, absPath }); + } + + if (touchesProtected) { + await requireCompanyAdmin(req.user.id, selected.companyId); + } + + for (const { f, parsed, absPath } of parsedEntries) { fs.mkdirSync(path.dirname(absPath), { recursive: true }); fs.writeFileSync(absPath, f.buffer); @@ -815,6 +832,15 @@ export function register(app, basePath = '') { return res.status(404).json({ error: 'Source file or directory not found' }); } + if ( + isProtectedAdminVfsPath(parsedOld.name) || + isProtectedAdminVfsPath(parsedNew.name) || + directoryContainsProtectedAdminVfsFile(absOld) || + directoryContainsProtectedAdminVfsFile(absNew) + ) { + await requireCompanyAdmin(req.user.id, selected.companyId); + } + fs.mkdirSync(path.dirname(absNew), { recursive: true }); fs.renameSync(absOld, absNew); @@ -961,6 +987,9 @@ export function register(app, basePath = '') { } try { + if (isProtectedAdminVfsPath(parsed.name)) { + await requireCompanyAdmin(req.user.id, selected.companyId); + } const vfsRoot = await resolveVfsRoot(req.user.id); const absPath = path.resolve(vfsRoot, parsed.name); if (!absPath.startsWith(vfsRoot)) { @@ -1058,6 +1087,9 @@ export function register(app, basePath = '') { } try { + if (isProtectedAdminVfsPath(parsed.name)) { + await requireCompanyAdmin(req.user.id, selected.companyId); + } const vfsRoot = await resolveVfsRoot(req.user.id); const absPath = path.resolve(vfsRoot, parsed.name); if (!absPath.startsWith(vfsRoot)) { @@ -1066,6 +1098,9 @@ export function register(app, basePath = '') { if (!fs.existsSync(absPath)) { return res.status(404).json({ error: 'File not found' }); } + if (directoryContainsProtectedAdminVfsFile(absPath)) { + await requireCompanyAdmin(req.user.id, selected.companyId); + } const stat = fs.statSync(absPath); if (stat.isDirectory()) { fs.rmSync(absPath, { recursive: true, force: true }); diff --git a/src/db/account.js b/src/db/account.js index 0c71043..6bd9642 100644 --- a/src/db/account.js +++ b/src/db/account.js @@ -1,4 +1,5 @@ import { getStore } from './store.js'; +import { requireSelectedCompanyAdmin } from './companyuser.js'; export async function list(googleAuthId) { const store = await getStore(); @@ -6,11 +7,13 @@ export async function list(googleAuthId) { } export async function create(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.account.create(googleAuthId, data); } export async function remove(googleAuthId, id) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.account.delete(googleAuthId, id); } diff --git a/src/db/agentprinter.js b/src/db/agentprinter.js index 714865a..9ccc442 100644 --- a/src/db/agentprinter.js +++ b/src/db/agentprinter.js @@ -1,4 +1,5 @@ import { getStore } from './store.js'; +import { requireSelectedCompanyAdmin } from './companyuser.js'; export async function list(googleAuthId, printeragentId) { const store = await getStore(); @@ -16,16 +17,19 @@ export async function findOrCreate(printeragentId, data) { } export async function update(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.agentprinter.update(googleAuthId, data); } export async function order(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.agentprinter.order(googleAuthId, data); } export async function remove(googleAuthId, id) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.agentprinter.delete(googleAuthId, id); } diff --git a/src/db/carrier.js b/src/db/carrier.js index 8e2aab2..05bbc2f 100644 --- a/src/db/carrier.js +++ b/src/db/carrier.js @@ -1,4 +1,5 @@ import { getStore } from './store.js'; +import { requireSelectedCompanyAdmin } from './companyuser.js'; export async function list(googleAuthId) { const store = await getStore(); @@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) { } export async function create(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.carrier.create(googleAuthId, data); } export async function update(googleAuthId, id, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.carrier.update(googleAuthId, id, data); } export async function order(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.carrier.order(googleAuthId, data); } export async function remove(googleAuthId, id) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.carrier.delete(googleAuthId, id); } diff --git a/src/db/company.js b/src/db/company.js index 775fb94..d901f5e 100644 --- a/src/db/company.js +++ b/src/db/company.js @@ -1,4 +1,5 @@ import { getStore, getProvider } from './store.js'; +import { requireCompanyAdmin } from './companyuser.js'; export async function listCompanies(authId) { const store = await getStore(); @@ -15,12 +16,18 @@ export async function createCompany(googleAuthId, data) { return store.company.create({ ...data, googleAuthId }); } -export async function updateCompany(id, data) { +export async function updateCompany(id, data, googleAuthId) { + if (googleAuthId != null) { + await requireCompanyAdmin(googleAuthId, id); + } const store = await getStore(); return store.company.update(id, data); } -export async function deleteCompany(companyId) { +export async function deleteCompany(companyId, googleAuthId) { + if (googleAuthId != null) { + await requireCompanyAdmin(googleAuthId, companyId); + } const store = await getStore(); await store.company.delete(companyId); } diff --git a/src/db/companysetting.js b/src/db/companysetting.js index 5180a1c..31f6e7f 100644 --- a/src/db/companysetting.js +++ b/src/db/companysetting.js @@ -1,4 +1,15 @@ import { getStore } from './store.js'; +import { requireSelectedCompanyAdmin } from './companyuser.js'; + +export const ADMIN_ONLY_SETTING_KEYS = new Set([ + 'ui.theme', + 'keys.tavily', + 'keys.openrouter', + 'keys.serper', + 'modelslugimage', + 'systemprompt-prefix', + 'systemprompt-suffix', +]); export async function list(googleAuthId, options) { const store = await getStore(); @@ -11,6 +22,9 @@ export async function getByKey(googleAuthId, key, options) { } export async function set(googleAuthId, input) { + if (ADMIN_ONLY_SETTING_KEYS.has(input?.key)) { + await requireSelectedCompanyAdmin(googleAuthId); + } const store = await getStore(); return store.companysetting.set(googleAuthId, input); } \ No newline at end of file diff --git a/src/db/companyuser.js b/src/db/companyuser.js index 76b3b2a..7bc0642 100644 --- a/src/db/companyuser.js +++ b/src/db/companyuser.js @@ -1,4 +1,53 @@ -import { getStore } from './store.js'; +import { getStore, getProvider } from './store.js'; + +export async function resolveCompanyAccess(googleAuthId, companyId) { + const provider = await getProvider(); + const access = await provider.query( + `SELECT company_id, role FROM companyuser WHERE company_id = @companyId AND google_auth_id = @googleAuthId`, + { companyId, googleAuthId } + ); + const accessRow = access.rows[0] ?? null; + if (!accessRow) { + const exists = await provider.query( + `SELECT id FROM company WHERE id = @companyId`, + { companyId } + ); + if (!exists.rows[0]) { + throw new Error('Company not found'); + } + throw new Error('Not authorized'); + } + return { + companyId: accessRow.company_id, + role: accessRow.role ?? 'user', + }; +} + +export async function resolveSelectedCompanyAccess(googleAuthId) { + const store = await getStore(); + const user = await store.google_auth.getById(googleAuthId); + const companyId = user?.selectedCompanyId ?? null; + if (companyId == null) { + throw new Error('No company selected'); + } + return resolveCompanyAccess(googleAuthId, companyId); +} + +export async function requireCompanyAdmin(googleAuthId, companyId) { + const ctx = await resolveCompanyAccess(googleAuthId, companyId); + if (ctx.role !== 'admin') { + throw new Error('Not authorized'); + } + return ctx; +} + +export async function requireSelectedCompanyAdmin(googleAuthId) { + const ctx = await resolveSelectedCompanyAccess(googleAuthId); + if (ctx.role !== 'admin') { + throw new Error('Not authorized'); + } + return ctx; +} export async function list(googleAuthId) { const store = await getStore(); diff --git a/src/db/customergroup.js b/src/db/customergroup.js index 7fb3c17..b61d4ab 100644 --- a/src/db/customergroup.js +++ b/src/db/customergroup.js @@ -1,4 +1,5 @@ import { getStore } from './store.js'; +import { requireSelectedCompanyAdmin } from './companyuser.js'; export async function list(googleAuthId) { const store = await getStore(); @@ -6,21 +7,25 @@ export async function list(googleAuthId) { } export async function create(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.customergroup.create(googleAuthId, data); } export async function update(googleAuthId, id, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.customergroup.update(googleAuthId, id, data); } export async function order(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.customergroup.order(googleAuthId, data); } export async function remove(googleAuthId, id) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.customergroup.delete(googleAuthId, id); } diff --git a/src/db/numbers.js b/src/db/numbers.js index faefefc..5936dcc 100644 --- a/src/db/numbers.js +++ b/src/db/numbers.js @@ -1,4 +1,5 @@ import { getStore } from './store.js'; +import { requireSelectedCompanyAdmin } from './companyuser.js'; export async function list(googleAuthId) { const store = await getStore(); @@ -6,6 +7,7 @@ export async function list(googleAuthId) { } export async function update(googleAuthId, id, input) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.numbers.update(googleAuthId, id, input); } diff --git a/src/db/posterminal.js b/src/db/posterminal.js index 88077bd..5333444 100644 --- a/src/db/posterminal.js +++ b/src/db/posterminal.js @@ -1,4 +1,5 @@ import { getStore, getProvider } from './store.js'; +import { requireSelectedCompanyAdmin } from './companyuser.js'; export async function list(googleAuthId) { const store = await getStore(); @@ -6,11 +7,13 @@ export async function list(googleAuthId) { } export async function upsert(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.posterminal.upsert(googleAuthId, data); } export async function remove(googleAuthId, id) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.posterminal.delete(googleAuthId, id); } diff --git a/src/db/printer.js b/src/db/printer.js index 2955ec9..8318a20 100644 --- a/src/db/printer.js +++ b/src/db/printer.js @@ -1,4 +1,5 @@ import { getStore } from './store.js'; +import { requireSelectedCompanyAdmin } from './companyuser.js'; export async function list(googleAuthId) { const store = await getStore(); @@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) { } export async function create(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.printer.create(googleAuthId, data); } export async function update(googleAuthId, id, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.printer.update(googleAuthId, id, data); } export async function order(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.printer.order(googleAuthId, data); } export async function remove(googleAuthId, id) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.printer.delete(googleAuthId, id); } diff --git a/src/db/printeragent.js b/src/db/printeragent.js index 4cabb9e..1a8c4b8 100644 --- a/src/db/printeragent.js +++ b/src/db/printeragent.js @@ -1,4 +1,5 @@ import { getStore } from './store.js'; +import { requireSelectedCompanyAdmin } from './companyuser.js'; export async function list(googleAuthId) { const store = await getStore(); @@ -6,16 +7,19 @@ export async function list(googleAuthId) { } export async function create(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.printeragent.create(googleAuthId, data); } export async function update(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.printeragent.update(googleAuthId, data); } export async function remove(googleAuthId, id) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.printeragent.delete(googleAuthId, id); } diff --git a/src/db/warehouse.js b/src/db/warehouse.js index 3b08736..b06b312 100644 --- a/src/db/warehouse.js +++ b/src/db/warehouse.js @@ -1,4 +1,5 @@ import { getStore } from './store.js'; +import { requireSelectedCompanyAdmin } from './companyuser.js'; export async function list(googleAuthId) { const store = await getStore(); @@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) { } export async function create(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.warehouse.create(googleAuthId, data); } export async function update(googleAuthId, id, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.warehouse.update(googleAuthId, id, data); } export async function order(googleAuthId, data) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.warehouse.order(googleAuthId, data); } export async function remove(googleAuthId, id) { + await requireSelectedCompanyAdmin(googleAuthId); const store = await getStore(); return store.warehouse.delete(googleAuthId, id); } diff --git a/src/services/javascriptSandbox.js b/src/services/javascriptSandbox.js index 604d720..b794f15 100644 --- a/src/services/javascriptSandbox.js +++ b/src/services/javascriptSandbox.js @@ -344,8 +344,8 @@ export async function runSandboxJob(options) { throw new Error('Sandbox worker pool is shutting down'); } - const { poolKey, googleAuthId, code, timeoutMs, dashbackToken, appletInput, appletFile } = options; - const job = { code, timeoutMs, dashbackToken, appletInput, appletFile, poolKey, googleAuthId }; + const { poolKey, googleAuthId, isAdmin = true, code, timeoutMs, dashbackToken, appletInput, appletFile } = options; + const job = { code, timeoutMs, dashbackToken, appletInput, appletFile, poolKey, googleAuthId, isAdmin }; const entry = await acquireWorker(poolKey, googleAuthId); if (entry) { diff --git a/src/tools/javascript-worker.js b/src/tools/javascript-worker.js index b721fac..9e835c3 100644 --- a/src/tools/javascript-worker.js +++ b/src/tools/javascript-worker.js @@ -544,7 +544,7 @@ async function runJob(job) { const appletInput = job.appletInput; const appletFile = job.appletFile; - const vfs = createVfs(vfsDir, { googleAuthId }); + const vfs = createVfs(vfsDir, { googleAuthId, isAdmin: job.isAdmin !== false }); const bridgedExcel = bridgeExcelJS(vfs); const bridgedPDF = bridgePDFKit(vfs); diff --git a/src/tools/javascript.js b/src/tools/javascript.js index 715eec0..bab47a4 100644 --- a/src/tools/javascript.js +++ b/src/tools/javascript.js @@ -5,6 +5,7 @@ import { defineTool, ToolError } from 'libllmorbiter'; import env from '../config/env.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { createVfs, prepareVfs } from './vfs.js'; +import { resolveSelectedCompanyAccess } from '../db/companyuser.js'; import { runSandboxJob } from '../services/javascriptSandbox.js'; const DEFAULT_TIMEOUT_MS = 20_000; @@ -184,11 +185,21 @@ function toTypedCsv(result, columns) { * * @param {{ code: string, timeoutMs: number, vfsRoot: string, googleAuthId: number, appletInput?: object, appletFile?: string }} ctx */ -function runInWorker({ code, timeoutMs, vfsRoot, googleAuthId, appletInput, appletFile }) { +async function runInWorker({ code, timeoutMs, vfsRoot, googleAuthId, appletInput, appletFile }) { + let isAdmin = true; + if (googleAuthId != null) { + try { + const access = await resolveSelectedCompanyAccess(googleAuthId); + isAdmin = access.role === 'admin'; + } catch { + isAdmin = false; + } + } const dashbackToken = jwt.sign({ id: googleAuthId }, env.jwtSecret, { expiresIn: '5m' }); return runSandboxJob({ poolKey: vfsRoot, googleAuthId, + isAdmin, code, timeoutMs, dashbackToken, diff --git a/src/tools/patch.js b/src/tools/patch.js index 6135e03..cd2a093 100644 --- a/src/tools/patch.js +++ b/src/tools/patch.js @@ -1,9 +1,9 @@ import fs from 'fs'; import path from 'path'; import { defineTool, ToolError } from 'libllmorbiter'; -import { isReadOnlyDatabasePath } from './vfs.js'; +import { isReadOnlyDatabasePath, isProtectedAdminVfsPath, prepareVfs } from './vfs.js'; +import { requireSelectedCompanyAdmin } from '../db/companyuser.js'; import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js'; -import { prepareVfs } from './vfs.js'; import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { getStore } from '../db/store.js'; @@ -155,6 +155,10 @@ export function createPatchTool({ googleAuthId }) { ); } + if (isProtectedAdminVfsPath(clean)) { + await requireSelectedCompanyAdmin(googleAuthId); + } + // A SKILL file must be flat — reject an attempted nested path up front. const looksSkillByExt = clean.toLowerCase().endsWith(SKILL_EXT.toLowerCase()); if (looksSkillByExt && clean.includes('/')) { diff --git a/src/tools/reset-vfs.js b/src/tools/reset-vfs.js index 1c25953..a776efe 100644 --- a/src/tools/reset-vfs.js +++ b/src/tools/reset-vfs.js @@ -2,7 +2,8 @@ import fs from 'fs'; import path from 'path'; import { defineTool } from 'libllmorbiter'; import { isSkillPath } from './skill.js'; -import { resolveVfsRoot } from './vfs.js'; +import { resolveVfsRoot, isProtectedAdminVfsPath, directoryContainsProtectedAdminVfsFile } from './vfs.js'; +import { resolveSelectedCompanyAccess, requireSelectedCompanyAdmin } from '../db/companyuser.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; /** @@ -40,6 +41,11 @@ export function createResetVfsTool({ googleAuthId }) { }, }, invoke: async ({ include_skills = false } = {}) => { + if (include_skills === true) { + await requireSelectedCompanyAdmin(googleAuthId); + } + const access = await resolveSelectedCompanyAccess(googleAuthId); + const isAdmin = access.role === 'admin'; const keepSkills = include_skills !== true; const keptSkills = []; const vfsRoot = await resolveVfsRoot(googleAuthId); @@ -51,8 +57,12 @@ export function createResetVfsTool({ googleAuthId }) { if (keepSkills && isSkillPath(entry)) { continue; } + const fullPath = path.join(vfsRoot, entry); + if (!isAdmin && (isProtectedAdminVfsPath(entry) || directoryContainsProtectedAdminVfsFile(fullPath))) { + continue; + } try { - fs.rmSync(path.join(vfsRoot, entry), { recursive: true, force: true }); + fs.rmSync(fullPath, { recursive: true, force: true }); deletedCount += 1; } catch (err) { console.warn(`[reset_vfs] could not remove "${entry}": ${err.message}`); diff --git a/src/tools/rm.js b/src/tools/rm.js index 4c5fa44..f95761c 100644 --- a/src/tools/rm.js +++ b/src/tools/rm.js @@ -1,7 +1,8 @@ import fs from 'fs'; import path from 'path'; import { defineTool, ToolError } from 'libllmorbiter'; -import { isReadOnlyDatabasePath, prepareVfs } from './vfs.js'; +import { isReadOnlyDatabasePath, isProtectedAdminVfsPath, prepareVfs } from './vfs.js'; +import { requireSelectedCompanyAdmin } from '../db/companyuser.js'; import { isSkillPath } from './skill.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; @@ -94,6 +95,10 @@ export function createRmTool({ googleAuthId }) { ); } + if (isProtectedAdminVfsPath(clean)) { + await requireSelectedCompanyAdmin(googleAuthId); + } + const vfsRoot = await prepareVfs({ googleAuthId }); const { abs, size } = deleteVfsFile(vfsRoot, clean); diff --git a/src/tools/vfs.js b/src/tools/vfs.js index 8aa6e24..28cdf15 100644 --- a/src/tools/vfs.js +++ b/src/tools/vfs.js @@ -1,6 +1,7 @@ import fs from 'fs'; import path from 'path'; import { getStore } from '../db/store.js'; +import { requireSelectedCompanyAdmin } from '../db/companyuser.js'; import { getDefaultSkills } from './skill.js'; import { getDefaultApplets, DEFAULT_APPLETS_VERSION } from './applet.js'; @@ -97,6 +98,39 @@ export function isReadOnlyDatabasePath(filePath) { return lower === 'image' || lower.startsWith('image/') || Boolean(extractFullHash(clean)); } +export function isProtectedAdminVfsPath(filePath) { + if (typeof filePath !== 'string' || !filePath.trim()) return false; + const clean = cleanVirtualPath(filePath, true); + const lower = clean.toLowerCase(); + return ( + lower.endsWith('.skill') || + lower.endsWith('.applet.js') || + lower.endsWith('.apple.js') || + lower === 'apple.js' || + lower.endsWith('/apple.js') + ); +} + +export function directoryContainsProtectedAdminVfsFile(absDirPath) { + try { + if (!fs.existsSync(absDirPath)) return false; + const stat = fs.statSync(absDirPath); + if (!stat.isDirectory()) return isProtectedAdminVfsPath(absDirPath); + const entries = fs.readdirSync(absDirPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(absDirPath, entry.name); + if (entry.isDirectory()) { + if (directoryContainsProtectedAdminVfsFile(fullPath)) return true; + } else if (isProtectedAdminVfsPath(entry.name)) { + return true; + } + } + return false; + } catch { + return false; + } +} + function assertNotReadOnlyDatabasePath(filePath) { if (isReadOnlyDatabasePath(filePath)) { throw new Error( @@ -190,6 +224,9 @@ export async function loadDefaultSkills(ctx = {}) { * @returns {Promise>} */ export async function restoreDefaultSkills(ctx = {}) { + if (ctx.googleAuthId != null) { + await requireSelectedCompanyAdmin(ctx.googleAuthId); + } return seedVfsFiles(ctx, getDefaultSkills(), { overwrite: true, label: 'skill' }); } @@ -208,6 +245,9 @@ export async function loadDefaultApplets(ctx = {}) { * @returns {Promise>} */ export async function restoreDefaultApplets(ctx = {}) { + if (ctx.googleAuthId != null) { + await requireSelectedCompanyAdmin(ctx.googleAuthId); + } return seedVfsFiles(ctx, getVersionedApplets(), { overwrite: true, label: 'applet' }); } @@ -293,7 +333,14 @@ function cbWrap(syncFn, arityWithOpt = 3) { * @param {{ googleAuthId: number }} [options] */ export function createVfs(vfsRoot, options = {}) { - const { googleAuthId } = options; + const { googleAuthId, isAdmin = true } = options; + + const assertWritablePath = (filePath) => { + assertNotReadOnlyDatabasePath(filePath); + if (!isAdmin && isProtectedAdminVfsPath(filePath)) { + throw new Error('Not authorized'); + } + }; // #region Internal Sandbox Helpers // MARK: - Internal Sandbox Helpers @@ -405,7 +452,7 @@ export function createVfs(vfsRoot, options = {}) { // --- Write & Append Operations --- writeFileSync: (p, data, opt) => { - assertNotReadOnlyDatabasePath(p); + assertWritablePath(p); const diskPath = safeResolve(p); fs.mkdirSync(path.dirname(diskPath), { recursive: true }); try { @@ -416,14 +463,14 @@ export function createVfs(vfsRoot, options = {}) { }, createWriteStream: (p, opt) => { - assertNotReadOnlyDatabasePath(p); + assertWritablePath(p); const diskPath = safeResolve(p); fs.mkdirSync(path.dirname(diskPath), { recursive: true }); return fs.createWriteStream(diskPath, opt); }, appendFileSync: (p, data, opt) => { - assertNotReadOnlyDatabasePath(p); + assertWritablePath(p); const diskPath = safeResolve(p); fs.mkdirSync(path.dirname(diskPath), { recursive: true }); try { @@ -435,7 +482,7 @@ export function createVfs(vfsRoot, options = {}) { // --- Copy, Rename & Truncate Operations --- copyFileSync: (src, dest, mode) => { - assertNotReadOnlyDatabasePath(dest); + assertWritablePath(dest); const hash = extractFullHash(src); if (hash) { const buf = syncFetchImage(hash); @@ -452,7 +499,7 @@ export function createVfs(vfsRoot, options = {}) { }, cpSync: (src, dest, opt) => { - assertNotReadOnlyDatabasePath(dest); + assertWritablePath(dest); const srcDisk = safeResolve(src); const destDisk = safeResolve(dest); fs.mkdirSync(path.dirname(destDisk), { recursive: true }); @@ -464,10 +511,13 @@ export function createVfs(vfsRoot, options = {}) { }, renameSync: (oldPath, newPath) => { - assertNotReadOnlyDatabasePath(oldPath); - assertNotReadOnlyDatabasePath(newPath); + assertWritablePath(oldPath); + assertWritablePath(newPath); const oldDisk = safeResolve(oldPath); const newDisk = safeResolve(newPath); + if (!isAdmin && (directoryContainsProtectedAdminVfsFile(oldDisk) || directoryContainsProtectedAdminVfsFile(newDisk))) { + throw new Error('Not authorized'); + } fs.mkdirSync(path.dirname(newDisk), { recursive: true }); try { return fs.renameSync(oldDisk, newDisk); @@ -477,7 +527,7 @@ export function createVfs(vfsRoot, options = {}) { }, truncateSync: (p, len) => { - assertNotReadOnlyDatabasePath(p); + assertWritablePath(p); const diskPath = safeResolve(p); try { return fs.truncateSync(diskPath, len); @@ -600,7 +650,7 @@ export function createVfs(vfsRoot, options = {}) { }, unlinkSync: (p) => { - assertNotReadOnlyDatabasePath(p); + assertWritablePath(p); const diskPath = safeResolve(p); try { return fs.unlinkSync(diskPath); @@ -610,8 +660,11 @@ export function createVfs(vfsRoot, options = {}) { }, rmdirSync: (p, opt) => { - assertNotReadOnlyDatabasePath(p); + assertWritablePath(p); const diskPath = safeResolve(p); + if (!isAdmin && directoryContainsProtectedAdminVfsFile(diskPath)) { + throw new Error('Not authorized'); + } try { return fs.rmdirSync(diskPath, opt); } catch (err) { @@ -620,8 +673,11 @@ export function createVfs(vfsRoot, options = {}) { }, rmSync: (p, opt) => { - assertNotReadOnlyDatabasePath(p); + assertWritablePath(p); const diskPath = safeResolve(p); + if (!isAdmin && directoryContainsProtectedAdminVfsFile(diskPath)) { + throw new Error('Not authorized'); + } try { return fs.rmSync(diskPath, opt); } catch (err) { @@ -662,7 +718,7 @@ export function createVfs(vfsRoot, options = {}) { writeFile: async (p, data, opt) => vfs.writeFileSync(p, data, opt), appendFile: async (p, data, opt) => vfs.appendFileSync(p, data, opt), copyFile: async (src, dest, mode) => { - assertNotReadOnlyDatabasePath(dest); + assertWritablePath(dest); const hash = extractFullHash(src); if (hash) { const buf = await vfs.promises.readFile(src); @@ -695,7 +751,7 @@ export function createVfs(vfsRoot, options = {}) { * containment + read-only guards as `writeFileSync`. */ resolveWritePath: (p) => { - assertNotReadOnlyDatabasePath(p); + assertWritablePath(p); return safeResolve(p); },